@stephenov/feedbackwidget 0.2.2 → 0.3.5

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.
@@ -0,0 +1,1457 @@
1
+ // src/utils.ts
2
+ function cn(...classes) {
3
+ return classes.filter(Boolean).join(" ");
4
+ }
5
+ function formatDuration(seconds) {
6
+ const mins = Math.floor(seconds / 60);
7
+ const secs = seconds % 60;
8
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
9
+ }
10
+ function getBrowserInfo() {
11
+ if (typeof window === "undefined") return {};
12
+ return {
13
+ userAgent: navigator.userAgent,
14
+ language: navigator.language,
15
+ viewport: {
16
+ width: window.innerWidth,
17
+ height: window.innerHeight
18
+ },
19
+ screen: {
20
+ width: screen.width,
21
+ height: screen.height
22
+ },
23
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
24
+ };
25
+ }
26
+ function isVoiceSupported() {
27
+ if (typeof window === "undefined") return false;
28
+ return !!(navigator.mediaDevices && typeof MediaRecorder !== "undefined");
29
+ }
30
+ var DEFAULT_LABELS = {
31
+ triggerTooltip: "Send feedback",
32
+ panelTitle: "Send Feedback",
33
+ voiceButton: "Voice",
34
+ textButton: "Type",
35
+ submitButton: "Send Feedback",
36
+ successMessage: "Thanks for your feedback!",
37
+ placeholder: "What's on your mind?"
38
+ };
39
+
40
+ // src/components/FeedbackTrigger.tsx
41
+ import { useState, useEffect, useRef, useCallback } from "react";
42
+
43
+ // src/components/icons.tsx
44
+ import { jsx, jsxs } from "react/jsx-runtime";
45
+ function ZapIcon({ className, size = 24 }) {
46
+ return /* @__PURE__ */ jsx(
47
+ "svg",
48
+ {
49
+ xmlns: "http://www.w3.org/2000/svg",
50
+ width: size,
51
+ height: size,
52
+ viewBox: "0 0 24 24",
53
+ fill: "none",
54
+ stroke: "currentColor",
55
+ strokeWidth: "2.5",
56
+ strokeLinecap: "round",
57
+ strokeLinejoin: "round",
58
+ className,
59
+ children: /* @__PURE__ */ jsx("polygon", { points: "13 2 3 14 12 14 11 22 21 10 12 10 13 2" })
60
+ }
61
+ );
62
+ }
63
+ function MicIcon({ className, size = 24 }) {
64
+ return /* @__PURE__ */ jsxs(
65
+ "svg",
66
+ {
67
+ xmlns: "http://www.w3.org/2000/svg",
68
+ width: size,
69
+ height: size,
70
+ viewBox: "0 0 24 24",
71
+ fill: "none",
72
+ stroke: "currentColor",
73
+ strokeWidth: "2",
74
+ strokeLinecap: "round",
75
+ strokeLinejoin: "round",
76
+ className,
77
+ children: [
78
+ /* @__PURE__ */ jsx("path", { d: "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" }),
79
+ /* @__PURE__ */ jsx("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
80
+ /* @__PURE__ */ jsx("line", { x1: "12", x2: "12", y1: "19", y2: "22" })
81
+ ]
82
+ }
83
+ );
84
+ }
85
+ function TypeIcon({ className, size = 24 }) {
86
+ return /* @__PURE__ */ jsxs(
87
+ "svg",
88
+ {
89
+ xmlns: "http://www.w3.org/2000/svg",
90
+ width: size,
91
+ height: size,
92
+ viewBox: "0 0 24 24",
93
+ fill: "none",
94
+ stroke: "currentColor",
95
+ strokeWidth: "2",
96
+ strokeLinecap: "round",
97
+ strokeLinejoin: "round",
98
+ className,
99
+ children: [
100
+ /* @__PURE__ */ jsx("polyline", { points: "4 7 4 4 20 4 20 7" }),
101
+ /* @__PURE__ */ jsx("line", { x1: "9", x2: "15", y1: "20", y2: "20" }),
102
+ /* @__PURE__ */ jsx("line", { x1: "12", x2: "12", y1: "4", y2: "20" })
103
+ ]
104
+ }
105
+ );
106
+ }
107
+ function XIcon({ className, size = 24 }) {
108
+ return /* @__PURE__ */ jsxs(
109
+ "svg",
110
+ {
111
+ xmlns: "http://www.w3.org/2000/svg",
112
+ width: size,
113
+ height: size,
114
+ viewBox: "0 0 24 24",
115
+ fill: "none",
116
+ stroke: "currentColor",
117
+ strokeWidth: "2",
118
+ strokeLinecap: "round",
119
+ strokeLinejoin: "round",
120
+ className,
121
+ children: [
122
+ /* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }),
123
+ /* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })
124
+ ]
125
+ }
126
+ );
127
+ }
128
+ function ImagePlusIcon({ className, size = 24 }) {
129
+ return /* @__PURE__ */ jsxs(
130
+ "svg",
131
+ {
132
+ xmlns: "http://www.w3.org/2000/svg",
133
+ width: size,
134
+ height: size,
135
+ viewBox: "0 0 24 24",
136
+ fill: "none",
137
+ stroke: "currentColor",
138
+ strokeWidth: "2",
139
+ strokeLinecap: "round",
140
+ strokeLinejoin: "round",
141
+ className,
142
+ children: [
143
+ /* @__PURE__ */ jsx("path", { d: "M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7" }),
144
+ /* @__PURE__ */ jsx("line", { x1: "16", x2: "22", y1: "5", y2: "5" }),
145
+ /* @__PURE__ */ jsx("line", { x1: "19", x2: "19", y1: "2", y2: "8" }),
146
+ /* @__PURE__ */ jsx("circle", { cx: "9", cy: "9", r: "2" }),
147
+ /* @__PURE__ */ jsx("path", { d: "m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" })
148
+ ]
149
+ }
150
+ );
151
+ }
152
+ function CheckIcon({ className, size = 24 }) {
153
+ return /* @__PURE__ */ jsx(
154
+ "svg",
155
+ {
156
+ xmlns: "http://www.w3.org/2000/svg",
157
+ width: size,
158
+ height: size,
159
+ viewBox: "0 0 24 24",
160
+ fill: "none",
161
+ stroke: "currentColor",
162
+ strokeWidth: "2",
163
+ strokeLinecap: "round",
164
+ strokeLinejoin: "round",
165
+ className,
166
+ children: /* @__PURE__ */ jsx("polyline", { points: "20 6 9 17 4 12" })
167
+ }
168
+ );
169
+ }
170
+ function LoaderIcon({ className, size = 24 }) {
171
+ return /* @__PURE__ */ jsx(
172
+ "svg",
173
+ {
174
+ xmlns: "http://www.w3.org/2000/svg",
175
+ width: size,
176
+ height: size,
177
+ viewBox: "0 0 24 24",
178
+ fill: "none",
179
+ stroke: "currentColor",
180
+ strokeWidth: "2",
181
+ strokeLinecap: "round",
182
+ strokeLinejoin: "round",
183
+ className,
184
+ style: { animation: "spin 1s linear infinite" },
185
+ children: /* @__PURE__ */ jsx("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" })
186
+ }
187
+ );
188
+ }
189
+ function SquareIcon({ className, size = 24 }) {
190
+ return /* @__PURE__ */ jsx(
191
+ "svg",
192
+ {
193
+ xmlns: "http://www.w3.org/2000/svg",
194
+ width: size,
195
+ height: size,
196
+ viewBox: "0 0 24 24",
197
+ fill: "currentColor",
198
+ stroke: "currentColor",
199
+ strokeWidth: "2",
200
+ strokeLinecap: "round",
201
+ strokeLinejoin: "round",
202
+ className,
203
+ children: /* @__PURE__ */ jsx("rect", { width: "14", height: "14", x: "5", y: "5", rx: "2" })
204
+ }
205
+ );
206
+ }
207
+ function AlertCircleIcon({ className, size = 24 }) {
208
+ return /* @__PURE__ */ jsxs(
209
+ "svg",
210
+ {
211
+ xmlns: "http://www.w3.org/2000/svg",
212
+ width: size,
213
+ height: size,
214
+ viewBox: "0 0 24 24",
215
+ fill: "none",
216
+ stroke: "currentColor",
217
+ strokeWidth: "2",
218
+ strokeLinecap: "round",
219
+ strokeLinejoin: "round",
220
+ className,
221
+ children: [
222
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
223
+ /* @__PURE__ */ jsx("line", { x1: "12", x2: "12", y1: "8", y2: "12" }),
224
+ /* @__PURE__ */ jsx("line", { x1: "12", x2: "12.01", y1: "16", y2: "16" })
225
+ ]
226
+ }
227
+ );
228
+ }
229
+
230
+ // src/components/FeedbackTrigger.tsx
231
+ import { jsx as jsx2 } from "react/jsx-runtime";
232
+ var POSITION_KEY = "feedbackwidget-trigger-position";
233
+ var DEFAULT_OFFSET = 24;
234
+ var BUTTON_SIZE = 52;
235
+ function FeedbackTrigger({
236
+ onClick,
237
+ isOpen,
238
+ position = "bottom-right",
239
+ accentColor = "#BDE0C2",
240
+ tooltip = "Send feedback"
241
+ }) {
242
+ const [coords, setCoords] = useState(null);
243
+ const [isDragging, setIsDragging] = useState(false);
244
+ const [dragStart, setDragStart] = useState(null);
245
+ const [hasMoved, setHasMoved] = useState(false);
246
+ const buttonRef = useRef(null);
247
+ const getDefaultPosition = useCallback(() => {
248
+ const w = typeof window !== "undefined" ? window.innerWidth : 1024;
249
+ const h = typeof window !== "undefined" ? window.innerHeight : 768;
250
+ switch (position) {
251
+ case "top-left":
252
+ return { x: DEFAULT_OFFSET, y: DEFAULT_OFFSET };
253
+ case "top-right":
254
+ return { x: w - BUTTON_SIZE - DEFAULT_OFFSET, y: DEFAULT_OFFSET };
255
+ case "bottom-left":
256
+ return { x: DEFAULT_OFFSET, y: h - BUTTON_SIZE - DEFAULT_OFFSET - 80 };
257
+ case "bottom-right":
258
+ default:
259
+ return { x: w - BUTTON_SIZE - DEFAULT_OFFSET, y: h - BUTTON_SIZE - DEFAULT_OFFSET - 80 };
260
+ }
261
+ }, [position]);
262
+ useEffect(() => {
263
+ const saved = localStorage.getItem(POSITION_KEY);
264
+ if (saved) {
265
+ try {
266
+ const parsed = JSON.parse(saved);
267
+ const maxX = window.innerWidth - BUTTON_SIZE - DEFAULT_OFFSET;
268
+ const maxY = window.innerHeight - BUTTON_SIZE - DEFAULT_OFFSET;
269
+ setCoords({
270
+ x: Math.min(Math.max(DEFAULT_OFFSET, parsed.x), maxX),
271
+ y: Math.min(Math.max(DEFAULT_OFFSET, parsed.y), maxY)
272
+ });
273
+ } catch {
274
+ setCoords(getDefaultPosition());
275
+ }
276
+ } else {
277
+ setCoords(getDefaultPosition());
278
+ }
279
+ }, [getDefaultPosition]);
280
+ useEffect(() => {
281
+ const handleResize = () => {
282
+ if (coords) {
283
+ const maxX = window.innerWidth - BUTTON_SIZE - DEFAULT_OFFSET;
284
+ const maxY = window.innerHeight - BUTTON_SIZE - DEFAULT_OFFSET;
285
+ const newX = Math.min(coords.x, maxX);
286
+ const newY = Math.min(coords.y, maxY);
287
+ if (newX !== coords.x || newY !== coords.y) {
288
+ setCoords({ x: newX, y: newY });
289
+ }
290
+ }
291
+ };
292
+ window.addEventListener("resize", handleResize);
293
+ return () => window.removeEventListener("resize", handleResize);
294
+ }, [coords]);
295
+ const savePosition = useCallback((pos) => {
296
+ localStorage.setItem(POSITION_KEY, JSON.stringify(pos));
297
+ }, []);
298
+ const handleMouseDown = (e) => {
299
+ if (e.button !== 0) return;
300
+ e.preventDefault();
301
+ setIsDragging(true);
302
+ setHasMoved(false);
303
+ setDragStart({ x: e.clientX, y: e.clientY });
304
+ };
305
+ const handleTouchStart = (e) => {
306
+ const touch = e.touches[0];
307
+ setIsDragging(true);
308
+ setHasMoved(false);
309
+ setDragStart({ x: touch.clientX, y: touch.clientY });
310
+ };
311
+ const handleMove = useCallback(
312
+ (clientX, clientY) => {
313
+ if (!isDragging || !dragStart || !coords) return;
314
+ const deltaX = clientX - dragStart.x;
315
+ const deltaY = clientY - dragStart.y;
316
+ if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
317
+ setHasMoved(true);
318
+ }
319
+ const maxX = window.innerWidth - BUTTON_SIZE - DEFAULT_OFFSET;
320
+ const maxY = window.innerHeight - BUTTON_SIZE - DEFAULT_OFFSET;
321
+ const newX = Math.min(Math.max(DEFAULT_OFFSET, coords.x + deltaX), maxX);
322
+ const newY = Math.min(Math.max(DEFAULT_OFFSET, coords.y + deltaY), maxY);
323
+ setCoords({ x: newX, y: newY });
324
+ setDragStart({ x: clientX, y: clientY });
325
+ },
326
+ [isDragging, dragStart, coords]
327
+ );
328
+ const handleMouseMove = useCallback(
329
+ (e) => handleMove(e.clientX, e.clientY),
330
+ [handleMove]
331
+ );
332
+ const handleTouchMove = useCallback(
333
+ (e) => handleMove(e.touches[0].clientX, e.touches[0].clientY),
334
+ [handleMove]
335
+ );
336
+ const handleEnd = useCallback(() => {
337
+ if (isDragging && coords) {
338
+ savePosition(coords);
339
+ }
340
+ setIsDragging(false);
341
+ setDragStart(null);
342
+ }, [isDragging, coords, savePosition]);
343
+ useEffect(() => {
344
+ if (isDragging) {
345
+ window.addEventListener("mousemove", handleMouseMove);
346
+ window.addEventListener("mouseup", handleEnd);
347
+ window.addEventListener("touchmove", handleTouchMove, { passive: false });
348
+ window.addEventListener("touchend", handleEnd);
349
+ }
350
+ return () => {
351
+ window.removeEventListener("mousemove", handleMouseMove);
352
+ window.removeEventListener("mouseup", handleEnd);
353
+ window.removeEventListener("touchmove", handleTouchMove);
354
+ window.removeEventListener("touchend", handleEnd);
355
+ };
356
+ }, [isDragging, handleMouseMove, handleTouchMove, handleEnd]);
357
+ const handleClick = () => {
358
+ if (!hasMoved) {
359
+ onClick();
360
+ }
361
+ };
362
+ if (!coords || isOpen) return null;
363
+ return /* @__PURE__ */ jsx2(
364
+ "button",
365
+ {
366
+ ref: buttonRef,
367
+ onMouseDown: handleMouseDown,
368
+ onTouchStart: handleTouchStart,
369
+ onClick: handleClick,
370
+ className: cn(
371
+ "fw-trigger",
372
+ isDragging && "fw-trigger--dragging"
373
+ ),
374
+ style: {
375
+ left: coords.x,
376
+ top: coords.y,
377
+ width: BUTTON_SIZE,
378
+ height: BUTTON_SIZE,
379
+ backgroundColor: accentColor
380
+ },
381
+ title: tooltip,
382
+ "aria-label": tooltip,
383
+ children: /* @__PURE__ */ jsx2(ZapIcon, { size: 24 })
384
+ }
385
+ );
386
+ }
387
+
388
+ // src/components/VoiceRecorder.tsx
389
+ import { useState as useState2, useRef as useRef2, useEffect as useEffect2, useCallback as useCallback2 } from "react";
390
+ import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
391
+ var MAX_DURATION_SECONDS = 120;
392
+ function VoiceRecorder({
393
+ onSubmit,
394
+ isSubmitting,
395
+ maxDuration = MAX_DURATION_SECONDS,
396
+ accentColor = "#BDE0C2"
397
+ }) {
398
+ const [isRecording, setIsRecording] = useState2(false);
399
+ const [duration, setDuration] = useState2(0);
400
+ const [error, setError] = useState2(null);
401
+ const [isSupported, setIsSupported] = useState2(true);
402
+ const [audioLevel, setAudioLevel] = useState2(0);
403
+ const mediaRecorderRef = useRef2(null);
404
+ const chunksRef = useRef2([]);
405
+ const timerRef = useRef2(null);
406
+ const streamRef = useRef2(null);
407
+ const analyzerRef = useRef2(null);
408
+ const animationRef = useRef2(null);
409
+ const durationRef = useRef2(0);
410
+ useEffect2(() => {
411
+ setIsSupported(isVoiceSupported());
412
+ }, []);
413
+ useEffect2(() => {
414
+ return () => {
415
+ stopRecording();
416
+ if (animationRef.current) {
417
+ cancelAnimationFrame(animationRef.current);
418
+ }
419
+ };
420
+ }, []);
421
+ const updateAudioLevel = useCallback2(() => {
422
+ if (!analyzerRef.current || !isRecording) return;
423
+ const dataArray = new Uint8Array(analyzerRef.current.frequencyBinCount);
424
+ analyzerRef.current.getByteFrequencyData(dataArray);
425
+ const average = dataArray.reduce((a, b) => a + b, 0) / dataArray.length;
426
+ setAudioLevel(average / 255);
427
+ animationRef.current = requestAnimationFrame(updateAudioLevel);
428
+ }, [isRecording]);
429
+ const startRecording = async () => {
430
+ setError(null);
431
+ chunksRef.current = [];
432
+ try {
433
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
434
+ streamRef.current = stream;
435
+ const audioContext = new AudioContext();
436
+ const source = audioContext.createMediaStreamSource(stream);
437
+ const analyzer = audioContext.createAnalyser();
438
+ analyzer.fftSize = 256;
439
+ source.connect(analyzer);
440
+ analyzerRef.current = analyzer;
441
+ const mimeType = MediaRecorder.isTypeSupported("audio/webm") ? "audio/webm" : MediaRecorder.isTypeSupported("audio/mp4") ? "audio/mp4" : "audio/ogg";
442
+ const mediaRecorder = new MediaRecorder(stream, { mimeType });
443
+ mediaRecorderRef.current = mediaRecorder;
444
+ mediaRecorder.ondataavailable = (e) => {
445
+ if (e.data.size > 0) {
446
+ chunksRef.current.push(e.data);
447
+ }
448
+ };
449
+ mediaRecorder.onstop = async () => {
450
+ const audioBlob = new Blob(chunksRef.current, { type: mimeType });
451
+ const finalDuration = durationRef.current;
452
+ if (streamRef.current) {
453
+ streamRef.current.getTracks().forEach((track) => track.stop());
454
+ streamRef.current = null;
455
+ }
456
+ if (audioBlob.size > 0 && finalDuration > 0) {
457
+ await onSubmit(audioBlob, finalDuration);
458
+ }
459
+ };
460
+ mediaRecorder.start(1e3);
461
+ setIsRecording(true);
462
+ setDuration(0);
463
+ durationRef.current = 0;
464
+ timerRef.current = setInterval(() => {
465
+ setDuration((prev) => {
466
+ const newDuration = prev + 1;
467
+ durationRef.current = newDuration;
468
+ if (newDuration >= maxDuration) {
469
+ stopRecording();
470
+ }
471
+ return newDuration;
472
+ });
473
+ }, 1e3);
474
+ animationRef.current = requestAnimationFrame(updateAudioLevel);
475
+ } catch (err) {
476
+ console.error("Error starting recording:", err);
477
+ if (err instanceof Error) {
478
+ if (err.name === "NotAllowedError") {
479
+ setError("Microphone access denied. Please allow microphone access.");
480
+ } else if (err.name === "NotFoundError") {
481
+ setError("No microphone found. Please connect a microphone.");
482
+ } else {
483
+ setError("Failed to start recording. Please try typing instead.");
484
+ }
485
+ }
486
+ }
487
+ };
488
+ const stopRecording = () => {
489
+ if (timerRef.current) {
490
+ clearInterval(timerRef.current);
491
+ timerRef.current = null;
492
+ }
493
+ if (animationRef.current) {
494
+ cancelAnimationFrame(animationRef.current);
495
+ animationRef.current = null;
496
+ }
497
+ if (mediaRecorderRef.current && mediaRecorderRef.current.state !== "inactive") {
498
+ mediaRecorderRef.current.stop();
499
+ }
500
+ setIsRecording(false);
501
+ setAudioLevel(0);
502
+ };
503
+ if (!isSupported) {
504
+ return /* @__PURE__ */ jsxs2("div", { className: "fw-voice-unsupported", children: [
505
+ /* @__PURE__ */ jsx3("div", { className: "fw-voice-error-icon", children: /* @__PURE__ */ jsx3(AlertCircleIcon, { size: 32 }) }),
506
+ /* @__PURE__ */ jsxs2("p", { children: [
507
+ "Voice recording not supported in this browser.",
508
+ /* @__PURE__ */ jsx3("br", {}),
509
+ "Please use the Type option instead."
510
+ ] })
511
+ ] });
512
+ }
513
+ if (error) {
514
+ return /* @__PURE__ */ jsxs2("div", { className: "fw-voice-error", children: [
515
+ /* @__PURE__ */ jsx3("div", { className: "fw-voice-error-icon fw-voice-error-icon--error", children: /* @__PURE__ */ jsx3(AlertCircleIcon, { size: 32 }) }),
516
+ /* @__PURE__ */ jsx3("p", { children: error }),
517
+ /* @__PURE__ */ jsx3(
518
+ "button",
519
+ {
520
+ onClick: () => setError(null),
521
+ className: "fw-voice-retry-btn",
522
+ children: "Try Again"
523
+ }
524
+ )
525
+ ] });
526
+ }
527
+ return /* @__PURE__ */ jsxs2("div", { className: "fw-voice-recorder", children: [
528
+ /* @__PURE__ */ jsxs2(
529
+ "button",
530
+ {
531
+ onClick: isRecording ? stopRecording : startRecording,
532
+ disabled: isSubmitting,
533
+ className: cn(
534
+ "fw-voice-btn",
535
+ isRecording && "fw-voice-btn--recording",
536
+ isSubmitting && "fw-voice-btn--disabled"
537
+ ),
538
+ style: {
539
+ backgroundColor: isRecording ? "#ef4444" : accentColor
540
+ },
541
+ children: [
542
+ isRecording && /* @__PURE__ */ jsx3(
543
+ "div",
544
+ {
545
+ className: "fw-voice-pulse",
546
+ style: {
547
+ transform: `scale(${1 + audioLevel * 0.3})`,
548
+ opacity: 0.5 + audioLevel * 0.5
549
+ }
550
+ }
551
+ ),
552
+ isSubmitting ? /* @__PURE__ */ jsx3(LoaderIcon, { size: 40, className: "fw-voice-icon fw-voice-icon--spin" }) : isRecording ? /* @__PURE__ */ jsx3(SquareIcon, { size: 40, className: "fw-voice-icon" }) : /* @__PURE__ */ jsx3(MicIcon, { size: 40, className: "fw-voice-icon" })
553
+ ]
554
+ }
555
+ ),
556
+ /* @__PURE__ */ jsx3("div", { className: "fw-voice-status", children: isSubmitting ? /* @__PURE__ */ jsx3("p", { className: "fw-voice-text", children: "Sending feedback..." }) : isRecording ? /* @__PURE__ */ jsxs2(Fragment, { children: [
557
+ /* @__PURE__ */ jsx3("p", { className: "fw-voice-timer", children: formatDuration(duration) }),
558
+ /* @__PURE__ */ jsx3("p", { className: "fw-voice-hint", children: "Tap to stop and send" }),
559
+ /* @__PURE__ */ jsxs2("p", { className: "fw-voice-limit", children: [
560
+ "Max ",
561
+ Math.floor(maxDuration / 60),
562
+ " minutes"
563
+ ] })
564
+ ] }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
565
+ /* @__PURE__ */ jsx3("p", { className: "fw-voice-cta", children: "Tap to record" }),
566
+ /* @__PURE__ */ jsx3("p", { className: "fw-voice-hint", children: "Share your feedback by voice" })
567
+ ] }) }),
568
+ isRecording && /* @__PURE__ */ jsx3("div", { className: "fw-voice-visualizer", children: [...Array(7)].map((_, i) => /* @__PURE__ */ jsx3(
569
+ "div",
570
+ {
571
+ className: "fw-voice-bar",
572
+ style: {
573
+ height: `${Math.max(8, audioLevel * 100 * Math.sin(i + Date.now() / 200) % 32)}px`,
574
+ backgroundColor: "#ef4444"
575
+ }
576
+ },
577
+ i
578
+ )) })
579
+ ] });
580
+ }
581
+
582
+ // src/api/client.ts
583
+ var API_BASE = "https://feedbackwidget-api.vercel.app/api/v1";
584
+ var FeedbackApiClient = class {
585
+ constructor(apiKey, baseUrl) {
586
+ this.apiKey = apiKey;
587
+ this.baseUrl = baseUrl || API_BASE;
588
+ }
589
+ /** Validate API key and get project config */
590
+ async validate() {
591
+ try {
592
+ const response = await fetch(`${this.baseUrl}/validate`, {
593
+ method: "GET",
594
+ headers: {
595
+ Authorization: `Bearer ${this.apiKey}`
596
+ }
597
+ });
598
+ if (!response.ok) {
599
+ const error = await response.json().catch(() => ({}));
600
+ return {
601
+ valid: false,
602
+ error: error.message || `HTTP ${response.status}`
603
+ };
604
+ }
605
+ return await response.json();
606
+ } catch (error) {
607
+ return {
608
+ valid: false,
609
+ error: error instanceof Error ? error.message : "Network error"
610
+ };
611
+ }
612
+ }
613
+ /** Submit feedback */
614
+ async submit(params) {
615
+ const formData = new FormData();
616
+ formData.append("type", params.type);
617
+ formData.append("page_path", params.pagePath);
618
+ formData.append("page_title", params.pageTitle);
619
+ formData.append("browser_info", JSON.stringify(getBrowserInfo()));
620
+ if (params.type === "text" && params.message) {
621
+ formData.append("message", params.message);
622
+ }
623
+ if (params.type === "voice" && params.audioBlob) {
624
+ formData.append("audio", params.audioBlob, "feedback.webm");
625
+ if (params.audioDuration) {
626
+ formData.append("audio_duration", String(Math.round(params.audioDuration)));
627
+ }
628
+ }
629
+ if (params.screenshots && params.screenshots.length > 0) {
630
+ params.screenshots.forEach((file, index) => {
631
+ formData.append(`screenshot_${index}`, file);
632
+ });
633
+ formData.append("screenshot_count", String(params.screenshots.length));
634
+ }
635
+ if (params.user) {
636
+ if (params.user.id) formData.append("user_id", params.user.id);
637
+ if (params.user.email) formData.append("user_email", params.user.email);
638
+ if (params.user.name) formData.append("user_name", params.user.name);
639
+ const { id, email, name, ...rest } = params.user;
640
+ if (Object.keys(rest).length > 0) {
641
+ formData.append("user_metadata", JSON.stringify(rest));
642
+ }
643
+ }
644
+ if (params.metadata) {
645
+ formData.append("custom_metadata", JSON.stringify(params.metadata));
646
+ }
647
+ const response = await fetch(`${this.baseUrl}/feedback`, {
648
+ method: "POST",
649
+ headers: {
650
+ Authorization: `Bearer ${this.apiKey}`
651
+ },
652
+ body: formData
653
+ });
654
+ if (!response.ok) {
655
+ const error = await response.json().catch(() => ({}));
656
+ throw new Error(error.message || error.error || `HTTP ${response.status}`);
657
+ }
658
+ return await response.json();
659
+ }
660
+ };
661
+ function createApiClient(apiKey, baseUrl) {
662
+ return new FeedbackApiClient(apiKey, baseUrl);
663
+ }
664
+
665
+ // src/components/FeedbackPanel.tsx
666
+ import * as React from "react";
667
+ import { useState as useState3, useRef as useRef3, useEffect as useEffect3 } from "react";
668
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
669
+ function FeedbackPanel({
670
+ isOpen,
671
+ onClose,
672
+ apiKey,
673
+ apiBaseUrl,
674
+ accentColor = "#BDE0C2",
675
+ allowVoice = true,
676
+ allowText = true,
677
+ allowScreenshots = true,
678
+ maxVoiceDuration = 120,
679
+ user,
680
+ metadata,
681
+ labels = {},
682
+ onSubmit,
683
+ onError
684
+ }) {
685
+ const mergedLabels = { ...DEFAULT_LABELS, ...labels };
686
+ const defaultMode = allowVoice ? "voice" : "text";
687
+ const [mode, setMode] = useState3(defaultMode);
688
+ const [message, setMessage] = useState3("");
689
+ const [screenshots, setScreenshots] = useState3([]);
690
+ const [isSubmitting, setIsSubmitting] = useState3(false);
691
+ const [isSuccess, setIsSuccess] = useState3(false);
692
+ const fileInputRef = useRef3(null);
693
+ const apiClient = React.useMemo(
694
+ () => createApiClient(apiKey, apiBaseUrl),
695
+ [apiKey, apiBaseUrl]
696
+ );
697
+ useEffect3(() => {
698
+ if (!isOpen) {
699
+ const timer = setTimeout(() => {
700
+ setMode(defaultMode);
701
+ setMessage("");
702
+ setScreenshots([]);
703
+ setIsSuccess(false);
704
+ }, 300);
705
+ return () => clearTimeout(timer);
706
+ }
707
+ }, [isOpen, defaultMode]);
708
+ const handleScreenshotUpload = (e) => {
709
+ const files = Array.from(e.target.files || []);
710
+ if (files.length === 0) return;
711
+ const remaining = 5 - screenshots.length;
712
+ if (remaining <= 0) return;
713
+ const newFiles = files.slice(0, remaining);
714
+ setScreenshots((prev) => [...prev, ...newFiles]);
715
+ if (fileInputRef.current) {
716
+ fileInputRef.current.value = "";
717
+ }
718
+ };
719
+ const removeScreenshot = (index) => {
720
+ setScreenshots((prev) => prev.filter((_, i) => i !== index));
721
+ };
722
+ const getPageInfo = () => ({
723
+ pagePath: typeof window !== "undefined" ? window.location.pathname : "/",
724
+ pageTitle: typeof document !== "undefined" ? document.title : "Unknown"
725
+ });
726
+ const handleVoiceSubmit = async (audioBlob, duration) => {
727
+ await submitFeedback("voice", void 0, audioBlob, duration);
728
+ };
729
+ const handleTextSubmit = async () => {
730
+ if (!message.trim()) return;
731
+ await submitFeedback("text", message.trim());
732
+ };
733
+ const submitFeedback = async (type, text, audioBlob, audioDuration) => {
734
+ setIsSubmitting(true);
735
+ try {
736
+ const { pagePath: pagePath2, pageTitle } = getPageInfo();
737
+ const result = await apiClient.submit({
738
+ type,
739
+ message: text,
740
+ audioBlob,
741
+ audioDuration,
742
+ screenshots,
743
+ pagePath: pagePath2,
744
+ pageTitle,
745
+ user,
746
+ metadata
747
+ });
748
+ setIsSuccess(true);
749
+ if (onSubmit) {
750
+ onSubmit({
751
+ id: result.id,
752
+ type,
753
+ message: text,
754
+ pagePath: pagePath2,
755
+ pageTitle,
756
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
757
+ });
758
+ }
759
+ setTimeout(() => {
760
+ onClose();
761
+ }, 1500);
762
+ } catch (error) {
763
+ console.error("Feedback submission error:", error);
764
+ if (onError && error instanceof Error) {
765
+ onError(error);
766
+ }
767
+ } finally {
768
+ setIsSubmitting(false);
769
+ }
770
+ };
771
+ const { pagePath } = getPageInfo();
772
+ return /* @__PURE__ */ jsxs3(Fragment2, { children: [
773
+ isOpen && /* @__PURE__ */ jsx4("div", { className: "fw-backdrop", onClick: onClose }),
774
+ /* @__PURE__ */ jsxs3(
775
+ "div",
776
+ {
777
+ className: cn(
778
+ "fw-panel",
779
+ isOpen ? "fw-panel--open" : "fw-panel--closed"
780
+ ),
781
+ children: [
782
+ /* @__PURE__ */ jsxs3("div", { className: "fw-panel-header", children: [
783
+ /* @__PURE__ */ jsx4("h2", { className: "fw-panel-title", children: mergedLabels.panelTitle }),
784
+ /* @__PURE__ */ jsx4("button", { onClick: onClose, className: "fw-panel-close", children: /* @__PURE__ */ jsx4(XIcon, { size: 20 }) })
785
+ ] }),
786
+ /* @__PURE__ */ jsx4("div", { className: "fw-panel-content", children: isSuccess ? /* @__PURE__ */ jsxs3("div", { className: "fw-success", children: [
787
+ /* @__PURE__ */ jsx4(
788
+ "div",
789
+ {
790
+ className: "fw-success-icon",
791
+ style: { backgroundColor: accentColor },
792
+ children: /* @__PURE__ */ jsx4(CheckIcon, { size: 40 })
793
+ }
794
+ ),
795
+ /* @__PURE__ */ jsx4("h3", { className: "fw-success-title", children: "Thanks!" }),
796
+ /* @__PURE__ */ jsx4("p", { className: "fw-success-text", children: mergedLabels.successMessage })
797
+ ] }) : /* @__PURE__ */ jsxs3(Fragment2, { children: [
798
+ /* @__PURE__ */ jsxs3("div", { className: "fw-context", children: [
799
+ /* @__PURE__ */ jsx4("span", { className: "fw-context-label", children: "Page:" }),
800
+ /* @__PURE__ */ jsx4("span", { className: "fw-context-value", children: pagePath })
801
+ ] }),
802
+ allowVoice && allowText && /* @__PURE__ */ jsxs3("div", { className: "fw-mode-selector", children: [
803
+ /* @__PURE__ */ jsxs3(
804
+ "button",
805
+ {
806
+ onClick: () => setMode("voice"),
807
+ disabled: isSubmitting,
808
+ className: cn(
809
+ "fw-mode-btn",
810
+ mode === "voice" && "fw-mode-btn--active"
811
+ ),
812
+ style: mode === "voice" ? { backgroundColor: accentColor } : {},
813
+ children: [
814
+ /* @__PURE__ */ jsx4(MicIcon, { size: 16 }),
815
+ mergedLabels.voiceButton
816
+ ]
817
+ }
818
+ ),
819
+ /* @__PURE__ */ jsxs3(
820
+ "button",
821
+ {
822
+ onClick: () => setMode("text"),
823
+ disabled: isSubmitting,
824
+ className: cn(
825
+ "fw-mode-btn",
826
+ mode === "text" && "fw-mode-btn--active"
827
+ ),
828
+ style: mode === "text" ? { backgroundColor: accentColor } : {},
829
+ children: [
830
+ /* @__PURE__ */ jsx4(TypeIcon, { size: 16 }),
831
+ mergedLabels.textButton
832
+ ]
833
+ }
834
+ )
835
+ ] }),
836
+ /* @__PURE__ */ jsx4("div", { className: "fw-input-area", children: mode === "voice" ? /* @__PURE__ */ jsx4(
837
+ VoiceRecorder,
838
+ {
839
+ onSubmit: handleVoiceSubmit,
840
+ isSubmitting,
841
+ maxDuration: maxVoiceDuration,
842
+ accentColor
843
+ }
844
+ ) : /* @__PURE__ */ jsxs3("div", { className: "fw-text-input", children: [
845
+ /* @__PURE__ */ jsx4(
846
+ "textarea",
847
+ {
848
+ value: message,
849
+ onChange: (e) => setMessage(e.target.value),
850
+ placeholder: mergedLabels.placeholder,
851
+ className: "fw-textarea",
852
+ disabled: isSubmitting
853
+ }
854
+ ),
855
+ /* @__PURE__ */ jsx4(
856
+ "button",
857
+ {
858
+ onClick: handleTextSubmit,
859
+ disabled: !message.trim() || isSubmitting,
860
+ className: "fw-submit-btn",
861
+ children: isSubmitting ? /* @__PURE__ */ jsxs3(Fragment2, { children: [
862
+ /* @__PURE__ */ jsx4(LoaderIcon, { size: 16, className: "fw-icon-spin" }),
863
+ "Sending..."
864
+ ] }) : mergedLabels.submitButton
865
+ }
866
+ )
867
+ ] }) }),
868
+ allowScreenshots && /* @__PURE__ */ jsxs3("div", { className: "fw-screenshots", children: [
869
+ /* @__PURE__ */ jsxs3("div", { className: "fw-screenshots-header", children: [
870
+ /* @__PURE__ */ jsx4("span", { className: "fw-screenshots-label", children: "Screenshots" }),
871
+ /* @__PURE__ */ jsxs3(
872
+ "button",
873
+ {
874
+ onClick: () => fileInputRef.current?.click(),
875
+ disabled: screenshots.length >= 5 || isSubmitting,
876
+ className: "fw-screenshots-add",
877
+ children: [
878
+ /* @__PURE__ */ jsx4(ImagePlusIcon, { size: 16 }),
879
+ "Add"
880
+ ]
881
+ }
882
+ ),
883
+ /* @__PURE__ */ jsx4(
884
+ "input",
885
+ {
886
+ ref: fileInputRef,
887
+ type: "file",
888
+ accept: "image/*",
889
+ multiple: true,
890
+ onChange: handleScreenshotUpload,
891
+ style: { display: "none" }
892
+ }
893
+ )
894
+ ] }),
895
+ screenshots.length > 0 ? /* @__PURE__ */ jsx4("div", { className: "fw-screenshots-grid", children: screenshots.map((file, index) => /* @__PURE__ */ jsxs3("div", { className: "fw-screenshot", children: [
896
+ /* @__PURE__ */ jsx4(
897
+ "img",
898
+ {
899
+ src: URL.createObjectURL(file),
900
+ alt: `Screenshot ${index + 1}`
901
+ }
902
+ ),
903
+ /* @__PURE__ */ jsx4(
904
+ "button",
905
+ {
906
+ onClick: () => removeScreenshot(index),
907
+ className: "fw-screenshot-remove",
908
+ children: /* @__PURE__ */ jsx4(XIcon, { size: 12 })
909
+ }
910
+ )
911
+ ] }, index)) }) : /* @__PURE__ */ jsx4("p", { className: "fw-screenshots-hint", children: "Optional: Add screenshots to illustrate your feedback" })
912
+ ] })
913
+ ] }) })
914
+ ]
915
+ }
916
+ )
917
+ ] });
918
+ }
919
+
920
+ // src/components/FeedbackWidget.tsx
921
+ import { useState as useState4, useEffect as useEffect4 } from "react";
922
+ import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
923
+ function FeedbackWidget({
924
+ apiKey,
925
+ position = "bottom-right",
926
+ theme = "light",
927
+ accentColor = "#BDE0C2",
928
+ allowVoice = true,
929
+ allowText = true,
930
+ allowScreenshots = true,
931
+ maxVoiceDuration = 120,
932
+ user,
933
+ metadata,
934
+ onOpen,
935
+ onClose,
936
+ onSubmit,
937
+ onError,
938
+ triggerComponent,
939
+ labels
940
+ }) {
941
+ const [isOpen, setIsOpen] = useState4(false);
942
+ const [mounted, setMounted] = useState4(false);
943
+ useEffect4(() => {
944
+ setMounted(true);
945
+ }, []);
946
+ const handleOpen = () => {
947
+ setIsOpen(true);
948
+ onOpen?.();
949
+ };
950
+ const handleClose = () => {
951
+ setIsOpen(false);
952
+ onClose?.();
953
+ };
954
+ if (!mounted) return null;
955
+ const apiBaseUrl = typeof window !== "undefined" ? window.__FEEDBACK_API_URL__ : void 0;
956
+ return /* @__PURE__ */ jsxs4(Fragment3, { children: [
957
+ /* @__PURE__ */ jsx5(FeedbackStyles, { theme }),
958
+ triggerComponent ? /* @__PURE__ */ jsx5("div", { onClick: handleOpen, children: triggerComponent }) : /* @__PURE__ */ jsx5(
959
+ FeedbackTrigger,
960
+ {
961
+ onClick: handleOpen,
962
+ isOpen,
963
+ position,
964
+ accentColor,
965
+ tooltip: labels?.triggerTooltip
966
+ }
967
+ ),
968
+ /* @__PURE__ */ jsx5(
969
+ FeedbackPanel,
970
+ {
971
+ isOpen,
972
+ onClose: handleClose,
973
+ apiKey,
974
+ apiBaseUrl,
975
+ accentColor,
976
+ allowVoice,
977
+ allowText,
978
+ allowScreenshots,
979
+ maxVoiceDuration,
980
+ user,
981
+ metadata,
982
+ labels,
983
+ onSubmit,
984
+ onError
985
+ }
986
+ )
987
+ ] });
988
+ }
989
+ function FeedbackStyles({ theme }) {
990
+ const styles = `
991
+ @keyframes fw-spin {
992
+ from { transform: rotate(0deg); }
993
+ to { transform: rotate(360deg); }
994
+ }
995
+
996
+ .fw-trigger {
997
+ position: fixed;
998
+ z-index: 9998;
999
+ display: flex;
1000
+ align-items: center;
1001
+ justify-content: center;
1002
+ border-radius: 50%;
1003
+ border: none;
1004
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
1005
+ cursor: pointer;
1006
+ transition: transform 0.15s, box-shadow 0.15s;
1007
+ user-select: none;
1008
+ touch-action: none;
1009
+ color: #0a0a0a;
1010
+ }
1011
+ .fw-trigger:hover {
1012
+ transform: scale(1.05);
1013
+ box-shadow: 0 6px 20px rgba(0,0,0,0.2);
1014
+ }
1015
+ .fw-trigger:active {
1016
+ transform: scale(0.95);
1017
+ }
1018
+ .fw-trigger--dragging {
1019
+ transform: scale(1.1);
1020
+ box-shadow: 0 8px 24px rgba(0,0,0,0.25);
1021
+ cursor: grabbing;
1022
+ }
1023
+
1024
+ .fw-backdrop {
1025
+ position: fixed;
1026
+ inset: 0;
1027
+ background: rgba(0,0,0,0.3);
1028
+ z-index: 9998;
1029
+ }
1030
+
1031
+ .fw-panel {
1032
+ position: fixed;
1033
+ top: 0;
1034
+ right: 0;
1035
+ height: 100%;
1036
+ width: 100%;
1037
+ max-width: 400px;
1038
+ background: ${theme === "dark" ? "#1a1a1a" : "#ffffff"};
1039
+ z-index: 9999;
1040
+ box-shadow: -4px 0 24px rgba(0,0,0,0.15);
1041
+ border-left: 1px solid ${theme === "dark" ? "#333" : "#e5e5e5"};
1042
+ transform: translateX(100%);
1043
+ transition: transform 0.3s ease-out;
1044
+ }
1045
+ .fw-panel--open {
1046
+ transform: translateX(0);
1047
+ }
1048
+ .fw-panel--closed {
1049
+ transform: translateX(100%);
1050
+ }
1051
+
1052
+ .fw-panel-header {
1053
+ display: flex;
1054
+ align-items: center;
1055
+ justify-content: space-between;
1056
+ padding: 16px 20px;
1057
+ border-bottom: 1px solid ${theme === "dark" ? "#333" : "#f0f0f0"};
1058
+ }
1059
+ .fw-panel-title {
1060
+ margin: 0;
1061
+ font-size: 18px;
1062
+ font-weight: 600;
1063
+ color: ${theme === "dark" ? "#fff" : "#0a0a0a"};
1064
+ }
1065
+ .fw-panel-close {
1066
+ padding: 8px;
1067
+ background: none;
1068
+ border: none;
1069
+ cursor: pointer;
1070
+ border-radius: 8px;
1071
+ color: ${theme === "dark" ? "#999" : "#666"};
1072
+ transition: background 0.15s;
1073
+ }
1074
+ .fw-panel-close:hover {
1075
+ background: ${theme === "dark" ? "#333" : "#f5f5f5"};
1076
+ }
1077
+
1078
+ .fw-panel-content {
1079
+ display: flex;
1080
+ flex-direction: column;
1081
+ height: calc(100% - 65px);
1082
+ overflow-y: auto;
1083
+ }
1084
+
1085
+ .fw-context {
1086
+ display: flex;
1087
+ align-items: center;
1088
+ gap: 8px;
1089
+ padding: 12px 20px;
1090
+ background: ${theme === "dark" ? "#222" : "#f9f9f9"};
1091
+ border-bottom: 1px solid ${theme === "dark" ? "#333" : "#f0f0f0"};
1092
+ font-size: 14px;
1093
+ }
1094
+ .fw-context-label {
1095
+ font-weight: 500;
1096
+ color: ${theme === "dark" ? "#999" : "#666"};
1097
+ }
1098
+ .fw-context-value {
1099
+ color: ${theme === "dark" ? "#ccc" : "#333"};
1100
+ overflow: hidden;
1101
+ text-overflow: ellipsis;
1102
+ white-space: nowrap;
1103
+ }
1104
+
1105
+ .fw-mode-selector {
1106
+ display: flex;
1107
+ gap: 8px;
1108
+ padding: 16px 20px;
1109
+ border-bottom: 1px solid ${theme === "dark" ? "#333" : "#f0f0f0"};
1110
+ }
1111
+ .fw-mode-btn {
1112
+ flex: 1;
1113
+ display: flex;
1114
+ align-items: center;
1115
+ justify-content: center;
1116
+ gap: 8px;
1117
+ padding: 10px 16px;
1118
+ border-radius: 8px;
1119
+ border: none;
1120
+ font-size: 14px;
1121
+ font-weight: 500;
1122
+ cursor: pointer;
1123
+ transition: background 0.15s;
1124
+ background: ${theme === "dark" ? "#333" : "#f0f0f0"};
1125
+ color: ${theme === "dark" ? "#ccc" : "#666"};
1126
+ }
1127
+ .fw-mode-btn:hover:not(:disabled) {
1128
+ background: ${theme === "dark" ? "#444" : "#e5e5e5"};
1129
+ }
1130
+ .fw-mode-btn--active {
1131
+ color: #0a0a0a;
1132
+ }
1133
+ .fw-mode-btn:disabled {
1134
+ opacity: 0.5;
1135
+ cursor: not-allowed;
1136
+ }
1137
+
1138
+ .fw-input-area {
1139
+ flex: 1;
1140
+ padding: 20px;
1141
+ }
1142
+
1143
+ .fw-text-input {
1144
+ display: flex;
1145
+ flex-direction: column;
1146
+ gap: 16px;
1147
+ }
1148
+ .fw-textarea {
1149
+ width: 100%;
1150
+ min-height: 150px;
1151
+ padding: 12px;
1152
+ border: 1px solid ${theme === "dark" ? "#444" : "#e5e5e5"};
1153
+ border-radius: 8px;
1154
+ font-size: 14px;
1155
+ font-family: inherit;
1156
+ resize: none;
1157
+ background: ${theme === "dark" ? "#222" : "#fff"};
1158
+ color: ${theme === "dark" ? "#fff" : "#0a0a0a"};
1159
+ }
1160
+ .fw-textarea:focus {
1161
+ outline: none;
1162
+ border-color: #BDE0C2;
1163
+ }
1164
+ .fw-textarea::placeholder {
1165
+ color: ${theme === "dark" ? "#666" : "#999"};
1166
+ }
1167
+ .fw-submit-btn {
1168
+ display: flex;
1169
+ align-items: center;
1170
+ justify-content: center;
1171
+ gap: 8px;
1172
+ width: 100%;
1173
+ padding: 12px 16px;
1174
+ background: #0a0a0a;
1175
+ color: #fff;
1176
+ border: none;
1177
+ border-radius: 8px;
1178
+ font-size: 14px;
1179
+ font-weight: 500;
1180
+ cursor: pointer;
1181
+ transition: background 0.15s;
1182
+ }
1183
+ .fw-submit-btn:hover:not(:disabled) {
1184
+ background: #1a1a1a;
1185
+ }
1186
+ .fw-submit-btn:disabled {
1187
+ opacity: 0.5;
1188
+ cursor: not-allowed;
1189
+ }
1190
+
1191
+ .fw-screenshots {
1192
+ padding: 16px 20px;
1193
+ border-top: 1px solid ${theme === "dark" ? "#333" : "#f0f0f0"};
1194
+ }
1195
+ .fw-screenshots-header {
1196
+ display: flex;
1197
+ align-items: center;
1198
+ justify-content: space-between;
1199
+ margin-bottom: 12px;
1200
+ }
1201
+ .fw-screenshots-label {
1202
+ font-size: 14px;
1203
+ font-weight: 500;
1204
+ color: ${theme === "dark" ? "#ccc" : "#333"};
1205
+ }
1206
+ .fw-screenshots-add {
1207
+ display: flex;
1208
+ align-items: center;
1209
+ gap: 6px;
1210
+ padding: 6px 12px;
1211
+ background: ${theme === "dark" ? "#333" : "#f0f0f0"};
1212
+ border: none;
1213
+ border-radius: 6px;
1214
+ font-size: 13px;
1215
+ cursor: pointer;
1216
+ color: ${theme === "dark" ? "#ccc" : "#666"};
1217
+ transition: background 0.15s;
1218
+ }
1219
+ .fw-screenshots-add:hover:not(:disabled) {
1220
+ background: ${theme === "dark" ? "#444" : "#e5e5e5"};
1221
+ }
1222
+ .fw-screenshots-add:disabled {
1223
+ opacity: 0.5;
1224
+ cursor: not-allowed;
1225
+ }
1226
+ .fw-screenshots-grid {
1227
+ display: flex;
1228
+ flex-wrap: wrap;
1229
+ gap: 8px;
1230
+ }
1231
+ .fw-screenshot {
1232
+ position: relative;
1233
+ }
1234
+ .fw-screenshot img {
1235
+ width: 64px;
1236
+ height: 64px;
1237
+ object-fit: cover;
1238
+ border-radius: 8px;
1239
+ border: 1px solid ${theme === "dark" ? "#444" : "#e5e5e5"};
1240
+ }
1241
+ .fw-screenshot-remove {
1242
+ position: absolute;
1243
+ top: -6px;
1244
+ right: -6px;
1245
+ width: 20px;
1246
+ height: 20px;
1247
+ display: flex;
1248
+ align-items: center;
1249
+ justify-content: center;
1250
+ background: #ef4444;
1251
+ color: #fff;
1252
+ border: none;
1253
+ border-radius: 50%;
1254
+ cursor: pointer;
1255
+ opacity: 0;
1256
+ transition: opacity 0.15s;
1257
+ }
1258
+ .fw-screenshot:hover .fw-screenshot-remove {
1259
+ opacity: 1;
1260
+ }
1261
+ .fw-screenshots-hint {
1262
+ margin: 0;
1263
+ font-size: 13px;
1264
+ color: ${theme === "dark" ? "#666" : "#999"};
1265
+ }
1266
+
1267
+ .fw-success {
1268
+ flex: 1;
1269
+ display: flex;
1270
+ flex-direction: column;
1271
+ align-items: center;
1272
+ justify-content: center;
1273
+ padding: 32px;
1274
+ }
1275
+ .fw-success-icon {
1276
+ width: 80px;
1277
+ height: 80px;
1278
+ border-radius: 50%;
1279
+ display: flex;
1280
+ align-items: center;
1281
+ justify-content: center;
1282
+ margin-bottom: 24px;
1283
+ color: #0a0a0a;
1284
+ }
1285
+ .fw-success-title {
1286
+ margin: 0 0 8px 0;
1287
+ font-size: 20px;
1288
+ font-weight: 600;
1289
+ color: ${theme === "dark" ? "#fff" : "#0a0a0a"};
1290
+ }
1291
+ .fw-success-text {
1292
+ margin: 0;
1293
+ color: ${theme === "dark" ? "#999" : "#666"};
1294
+ }
1295
+
1296
+ /* Voice Recorder */
1297
+ .fw-voice-recorder {
1298
+ display: flex;
1299
+ flex-direction: column;
1300
+ align-items: center;
1301
+ justify-content: center;
1302
+ padding: 32px 0;
1303
+ }
1304
+ .fw-voice-btn {
1305
+ position: relative;
1306
+ width: 96px;
1307
+ height: 96px;
1308
+ border-radius: 50%;
1309
+ border: none;
1310
+ display: flex;
1311
+ align-items: center;
1312
+ justify-content: center;
1313
+ cursor: pointer;
1314
+ transition: transform 0.15s, box-shadow 0.15s;
1315
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
1316
+ color: #0a0a0a;
1317
+ }
1318
+ .fw-voice-btn:hover:not(:disabled) {
1319
+ transform: scale(1.05);
1320
+ box-shadow: 0 6px 20px rgba(0,0,0,0.2);
1321
+ }
1322
+ .fw-voice-btn:active:not(:disabled) {
1323
+ transform: scale(0.95);
1324
+ }
1325
+ .fw-voice-btn--recording {
1326
+ box-shadow: 0 4px 20px rgba(239,68,68,0.4);
1327
+ }
1328
+ .fw-voice-btn--recording:hover {
1329
+ box-shadow: 0 6px 24px rgba(239,68,68,0.5);
1330
+ }
1331
+ .fw-voice-btn--disabled {
1332
+ opacity: 0.5;
1333
+ cursor: not-allowed;
1334
+ }
1335
+ .fw-voice-pulse {
1336
+ position: absolute;
1337
+ inset: 0;
1338
+ border-radius: 50%;
1339
+ border: 4px solid rgba(239,68,68,0.3);
1340
+ animation: fw-pulse 1s infinite;
1341
+ }
1342
+ @keyframes fw-pulse {
1343
+ 0%, 100% { opacity: 0.5; }
1344
+ 50% { opacity: 1; }
1345
+ }
1346
+ .fw-voice-icon {
1347
+ color: #fff;
1348
+ }
1349
+ .fw-voice-icon--spin {
1350
+ animation: fw-spin 1s linear infinite;
1351
+ }
1352
+ .fw-voice-status {
1353
+ margin-top: 24px;
1354
+ text-align: center;
1355
+ }
1356
+ .fw-voice-timer {
1357
+ font-size: 32px;
1358
+ font-family: monospace;
1359
+ font-weight: 600;
1360
+ color: ${theme === "dark" ? "#fff" : "#0a0a0a"};
1361
+ margin: 0;
1362
+ }
1363
+ .fw-voice-cta {
1364
+ font-size: 16px;
1365
+ font-weight: 500;
1366
+ color: ${theme === "dark" ? "#ccc" : "#333"};
1367
+ margin: 0;
1368
+ }
1369
+ .fw-voice-hint {
1370
+ font-size: 14px;
1371
+ color: ${theme === "dark" ? "#666" : "#999"};
1372
+ margin: 4px 0 0 0;
1373
+ }
1374
+ .fw-voice-limit {
1375
+ font-size: 12px;
1376
+ color: ${theme === "dark" ? "#555" : "#bbb"};
1377
+ margin: 8px 0 0 0;
1378
+ }
1379
+ .fw-voice-text {
1380
+ color: ${theme === "dark" ? "#999" : "#666"};
1381
+ margin: 0;
1382
+ }
1383
+ .fw-voice-visualizer {
1384
+ display: flex;
1385
+ align-items: flex-end;
1386
+ justify-content: center;
1387
+ gap: 4px;
1388
+ height: 32px;
1389
+ margin-top: 24px;
1390
+ }
1391
+ .fw-voice-bar {
1392
+ width: 6px;
1393
+ border-radius: 3px;
1394
+ transition: height 0.075s;
1395
+ }
1396
+ .fw-voice-unsupported,
1397
+ .fw-voice-error {
1398
+ display: flex;
1399
+ flex-direction: column;
1400
+ align-items: center;
1401
+ justify-content: center;
1402
+ padding: 32px;
1403
+ text-align: center;
1404
+ }
1405
+ .fw-voice-error-icon {
1406
+ width: 64px;
1407
+ height: 64px;
1408
+ border-radius: 50%;
1409
+ background: ${theme === "dark" ? "#333" : "#f5f5f5"};
1410
+ display: flex;
1411
+ align-items: center;
1412
+ justify-content: center;
1413
+ margin-bottom: 16px;
1414
+ color: ${theme === "dark" ? "#666" : "#999"};
1415
+ }
1416
+ .fw-voice-error-icon--error {
1417
+ background: #fef2f2;
1418
+ color: #ef4444;
1419
+ }
1420
+ .fw-voice-unsupported p,
1421
+ .fw-voice-error p {
1422
+ margin: 0;
1423
+ font-size: 14px;
1424
+ color: ${theme === "dark" ? "#999" : "#666"};
1425
+ }
1426
+ .fw-voice-retry-btn {
1427
+ margin-top: 16px;
1428
+ padding: 8px 16px;
1429
+ background: ${theme === "dark" ? "#333" : "#f0f0f0"};
1430
+ border: none;
1431
+ border-radius: 6px;
1432
+ font-size: 14px;
1433
+ cursor: pointer;
1434
+ color: ${theme === "dark" ? "#ccc" : "#333"};
1435
+ }
1436
+ .fw-voice-retry-btn:hover {
1437
+ background: ${theme === "dark" ? "#444" : "#e5e5e5"};
1438
+ }
1439
+
1440
+ .fw-icon-spin {
1441
+ animation: fw-spin 1s linear infinite;
1442
+ }
1443
+ `;
1444
+ return /* @__PURE__ */ jsx5("style", { dangerouslySetInnerHTML: { __html: styles } });
1445
+ }
1446
+
1447
+ export {
1448
+ cn,
1449
+ formatDuration,
1450
+ isVoiceSupported,
1451
+ FeedbackTrigger,
1452
+ VoiceRecorder,
1453
+ FeedbackApiClient,
1454
+ createApiClient,
1455
+ FeedbackPanel,
1456
+ FeedbackWidget
1457
+ };