reflet-sdk 0.1.4 → 0.2.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/react.js CHANGED
@@ -1,355 +1,1263 @@
1
- import { Reflet } from './chunk-U7TQSXOE.js';
2
- export { Reflet } from './chunk-U7TQSXOE.js';
3
- import { createContext, useState, useMemo, useEffect, useContext, useCallback } from 'react';
4
- import { jsx } from 'react/jsx-runtime';
5
-
6
- var RefletContext = createContext(null);
7
- function RefletProvider({
8
- publicKey,
9
- baseUrl,
10
- user: initialUser,
11
- userToken,
12
- children
13
- }) {
14
- const [user, setUser] = useState(initialUser);
15
- const [isReady, setIsReady] = useState(false);
16
- const client = useMemo(() => {
17
- const config = {
18
- publicKey,
19
- baseUrl,
20
- user,
21
- userToken
22
- };
23
- return new Reflet(config);
24
- }, [publicKey, baseUrl, user, userToken]);
25
- useEffect(() => {
26
- if (userToken) {
27
- client.setUserToken(userToken);
28
- } else {
29
- client.setUser(user);
30
- }
31
- }, [client, user, userToken]);
32
- useEffect(() => {
33
- setIsReady(true);
34
- }, []);
35
- const value = useMemo(
36
- () => ({
37
- client,
38
- isReady,
39
- user,
40
- setUser
41
- }),
42
- [client, isReady, user]
43
- );
44
- return /* @__PURE__ */ jsx(RefletContext.Provider, { value, children });
45
- }
46
- function useRefletContext() {
47
- const context = useContext(RefletContext);
48
- if (!context) {
49
- throw new Error("useRefletContext must be used within a RefletProvider");
1
+ import { t as Reflet } from "./client-BYuSKJw5.js";
2
+ import { i as useRefletContext, n as RefletProvider, r as useRefletClient, t as RefletContext } from "./react-context-DZw_HHjj.js";
3
+ import { cloneElement, isValidElement, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
4
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
+ //#region src/react-changelog-widget.tsx
6
+ /**
7
+ * React component that wraps the Reflet Changelog Widget.
8
+ *
9
+ * Dynamically loads and initializes a RefletChangelogWidget instance.
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * import { ChangelogWidget } from 'reflet-sdk/react';
14
+ *
15
+ * export function MyApp() {
16
+ * return <ChangelogWidget publicKey="fb_pub_xxx" mode="card" theme="auto" />;
17
+ * }
18
+ * ```
19
+ */
20
+ function ChangelogWidget({ publicKey, mode = "card", position = "bottom-right", theme = "light", primaryColor, maxEntries, triggerSelector, autoOpenForNew, locale, onOpen, onClose, onEntryClick }) {
21
+ const widgetRef = useRef(null);
22
+ const initWidget = useCallback(async () => {
23
+ const WidgetClass = window.RefletChangelogWidget;
24
+ if (WidgetClass) {
25
+ const instance = new WidgetClass({
26
+ autoOpenForNew,
27
+ locale,
28
+ maxEntries,
29
+ mode,
30
+ onClose,
31
+ onEntryClick,
32
+ onOpen,
33
+ position,
34
+ primaryColor,
35
+ publicKey,
36
+ theme,
37
+ triggerSelector
38
+ });
39
+ widgetRef.current = instance;
40
+ await instance.init();
41
+ return;
42
+ }
43
+ if (!document.querySelector("script[src*=\"reflet-changelog\"]")) {
44
+ const script = document.createElement("script");
45
+ script.src = "https://cdn.reflet.app/widget/reflet-changelog.v1.js";
46
+ script.async = true;
47
+ script.onload = () => {
48
+ const LoadedClass = window.RefletChangelogWidget;
49
+ if (LoadedClass) {
50
+ const instance = new LoadedClass({
51
+ autoOpenForNew,
52
+ locale,
53
+ maxEntries,
54
+ mode,
55
+ onClose,
56
+ onEntryClick,
57
+ onOpen,
58
+ position,
59
+ primaryColor,
60
+ publicKey,
61
+ theme,
62
+ triggerSelector
63
+ });
64
+ widgetRef.current = instance;
65
+ instance.init();
66
+ }
67
+ };
68
+ document.head.appendChild(script);
69
+ }
70
+ }, [
71
+ publicKey,
72
+ mode,
73
+ position,
74
+ theme,
75
+ primaryColor,
76
+ maxEntries,
77
+ triggerSelector,
78
+ autoOpenForNew,
79
+ locale,
80
+ onOpen,
81
+ onClose,
82
+ onEntryClick
83
+ ]);
84
+ useEffect(() => {
85
+ initWidget();
86
+ return () => {
87
+ if (widgetRef.current) {
88
+ widgetRef.current.destroy();
89
+ widgetRef.current = null;
90
+ }
91
+ };
92
+ }, [initWidget]);
93
+ return null;
94
+ }
95
+ //#endregion
96
+ //#region src/react-feedback-dialog-types.ts
97
+ const DEFAULT_LABELS = {
98
+ cancel: "Cancel",
99
+ categoryBug: "Bug",
100
+ categoryFeature: "Feature",
101
+ categoryQuestion: "Question",
102
+ descriptionPlaceholder: "Tell us more... (optional)",
103
+ emailLabel: "Email",
104
+ emailPlaceholder: "your@email.com (optional)",
105
+ required: "Required",
106
+ submit: "Send Feedback",
107
+ successMessage: "Your feedback has been received. We appreciate your input.",
108
+ successTitle: "Thank you!",
109
+ title: "Send Feedback",
110
+ titlePlaceholder: "What's on your mind?"
111
+ };
112
+ const CATEGORY_ICONS = {
113
+ bug: "🐛",
114
+ feature: "✨",
115
+ question: "💬"
116
+ };
117
+ const AUTO_CLOSE_DELAY = 2500;
118
+ //#endregion
119
+ //#region src/react-feedback-styles-parts.ts
120
+ function getFeedbackChromeCSS() {
121
+ return `
122
+ /* ===== Footer ===== */
123
+ .reflet-footer {
124
+ display: flex;
125
+ justify-content: flex-end;
126
+ gap: 8px;
127
+ padding-top: 4px;
128
+ }
129
+
130
+ .reflet-btn {
131
+ padding: 10px 20px;
132
+ border: none;
133
+ border-radius: var(--reflet-radius-sm);
134
+ font-family: var(--reflet-font);
135
+ font-size: 14px;
136
+ font-weight: 500;
137
+ cursor: pointer;
138
+ transition: all 150ms;
139
+ display: inline-flex;
140
+ align-items: center;
141
+ justify-content: center;
142
+ gap: 6px;
143
+ line-height: 1;
144
+ }
145
+
146
+ .reflet-btn-primary {
147
+ background: var(--reflet-primary);
148
+ color: #ffffff;
149
+ }
150
+
151
+ .reflet-btn-primary:hover:not(:disabled) {
152
+ background: var(--reflet-primary-hover);
153
+ }
154
+
155
+ .reflet-btn-primary:disabled {
156
+ opacity: 0.5;
157
+ cursor: not-allowed;
158
+ }
159
+
160
+ .reflet-btn-secondary {
161
+ background: transparent;
162
+ color: var(--reflet-text-secondary);
163
+ }
164
+
165
+ .reflet-btn-secondary:hover {
166
+ background: var(--reflet-bg-secondary);
167
+ color: var(--reflet-text);
168
+ }
169
+
170
+ /* ===== Spinner ===== */
171
+ .reflet-spinner {
172
+ width: 16px;
173
+ height: 16px;
174
+ border: 2px solid rgba(255,255,255,0.3);
175
+ border-top-color: #ffffff;
176
+ border-radius: 50%;
177
+ animation: reflet-spin 600ms linear infinite;
178
+ }
179
+
180
+ /* ===== Success State ===== */
181
+ .reflet-success {
182
+ display: flex;
183
+ flex-direction: column;
184
+ align-items: center;
185
+ justify-content: center;
186
+ padding: 48px 24px;
187
+ text-align: center;
188
+ gap: 16px;
189
+ }
190
+
191
+ .reflet-success-icon {
192
+ width: 56px;
193
+ height: 56px;
194
+ border-radius: 50%;
195
+ background: color-mix(in srgb, var(--reflet-success) 12%, transparent);
196
+ display: flex;
197
+ align-items: center;
198
+ justify-content: center;
199
+ }
200
+
201
+ .reflet-success-icon svg {
202
+ width: 28px;
203
+ height: 28px;
204
+ color: var(--reflet-success);
205
+ }
206
+
207
+ .reflet-success-icon svg path {
208
+ stroke-dasharray: 24;
209
+ stroke-dashoffset: 24;
210
+ animation: reflet-checkmark 400ms ease-out 100ms forwards;
211
+ }
212
+
213
+ .reflet-success-title {
214
+ font-size: 18px;
215
+ font-weight: 600;
216
+ color: var(--reflet-text);
217
+ margin: 0;
218
+ }
219
+
220
+ .reflet-success-text {
221
+ font-size: 14px;
222
+ color: var(--reflet-text-secondary);
223
+ margin: 0;
224
+ }
225
+
226
+ /* ===== Error State ===== */
227
+ .reflet-error-msg {
228
+ font-size: 13px;
229
+ color: var(--reflet-error);
230
+ margin: 0;
231
+ }
232
+
233
+ /* ===== Trigger Button ===== */
234
+ .reflet-trigger {
235
+ display: inline-flex;
236
+ align-items: center;
237
+ gap: 8px;
238
+ padding: 10px 18px;
239
+ background: var(--reflet-primary);
240
+ color: #ffffff;
241
+ border: none;
242
+ border-radius: var(--reflet-radius-sm);
243
+ font-family: var(--reflet-font);
244
+ font-size: 14px;
245
+ font-weight: 500;
246
+ cursor: pointer;
247
+ transition: background 150ms;
248
+ line-height: 1;
249
+ }
250
+
251
+ .reflet-trigger:hover {
252
+ background: var(--reflet-primary-hover);
253
+ }
254
+
255
+ .reflet-trigger svg {
256
+ width: 16px;
257
+ height: 16px;
258
+ }
259
+
260
+ /* ===== Honeypot ===== */
261
+ .reflet-hp {
262
+ position: absolute;
263
+ top: -9999px;
264
+ left: -9999px;
265
+ opacity: 0;
266
+ height: 0;
267
+ width: 0;
268
+ z-index: -1;
269
+ }
270
+
271
+ /* ===== Powered by ===== */
272
+ .reflet-powered {
273
+ text-align: center;
274
+ padding: 0 24px 16px;
275
+ font-size: 11px;
276
+ color: var(--reflet-text-tertiary);
277
+ }
278
+
279
+ .reflet-powered a {
280
+ color: var(--reflet-text-secondary);
281
+ text-decoration: none;
282
+ font-weight: 500;
283
+ }
284
+
285
+ .reflet-powered a:hover {
286
+ color: var(--reflet-primary);
287
+ }
288
+
289
+ /* ===== Mobile handle ===== */
290
+ @media (max-width: 640px) {
291
+ .reflet-header::before {
292
+ content: '';
293
+ position: absolute;
294
+ top: 8px;
295
+ left: 50%;
296
+ transform: translateX(-50%);
297
+ width: 36px;
298
+ height: 4px;
299
+ border-radius: 2px;
300
+ background: var(--reflet-border);
301
+ }
302
+
303
+ .reflet-header {
304
+ position: relative;
305
+ padding-top: 24px;
50
306
  }
51
- return context;
52
- }
53
- function useRefletClient() {
54
- const { client } = useRefletContext();
55
- return client;
56
- }
57
- function useBoardConfig() {
58
- const client = useRefletClient();
59
- const [data, setData] = useState();
60
- const [isLoading, setIsLoading] = useState(true);
61
- const [error, setError] = useState(null);
62
- const fetch = useCallback(async () => {
63
- setIsLoading(true);
64
- setError(null);
65
- try {
66
- const result = await client.getConfig();
67
- setData(result);
68
- } catch (err) {
69
- setError(
70
- err instanceof Error ? err : new Error("Failed to fetch board config")
71
- );
72
- } finally {
73
- setIsLoading(false);
74
- }
75
- }, [client]);
76
- useEffect(() => {
77
- fetch();
78
- }, [fetch]);
79
- return { data, isLoading, error, refetch: fetch };
80
307
  }
81
- function useFeedbackList(options = {}) {
82
- const client = useRefletClient();
83
- const { enabled = true, ...params } = options;
84
- const { statusId, status, search, sortBy, limit, offset } = params;
85
- const [data, setData] = useState();
86
- const [isLoading, setIsLoading] = useState(enabled);
87
- const [error, setError] = useState(null);
88
- const fetch = useCallback(async () => {
89
- setIsLoading(true);
90
- setError(null);
91
- try {
92
- const result = await client.list({
93
- statusId,
94
- status,
95
- search,
96
- sortBy,
97
- limit,
98
- offset
99
- });
100
- setData(result);
101
- } catch (err) {
102
- setError(
103
- err instanceof Error ? err : new Error("Failed to fetch feedback")
104
- );
105
- } finally {
106
- setIsLoading(false);
107
- }
108
- }, [client, statusId, status, search, sortBy, limit, offset]);
109
- useEffect(() => {
110
- if (enabled) {
111
- fetch();
112
- }
113
- }, [fetch, enabled]);
114
- return { data, isLoading, error, refetch: fetch };
308
+ `;
115
309
  }
116
- function useFeedback(feedbackId) {
117
- const client = useRefletClient();
118
- const [data, setData] = useState();
119
- const [isLoading, setIsLoading] = useState(!!feedbackId);
120
- const [error, setError] = useState(null);
121
- const fetch = useCallback(async () => {
122
- if (!feedbackId) {
123
- setData(void 0);
124
- setIsLoading(false);
125
- return;
126
- }
127
- setIsLoading(true);
128
- setError(null);
129
- try {
130
- const result = await client.get(feedbackId);
131
- setData(result);
132
- } catch (err) {
133
- setError(
134
- err instanceof Error ? err : new Error("Failed to fetch feedback")
135
- );
136
- } finally {
137
- setIsLoading(false);
138
- }
139
- }, [client, feedbackId]);
140
- useEffect(() => {
141
- fetch();
142
- }, [fetch]);
143
- return { data, isLoading, error, refetch: fetch };
310
+ //#endregion
311
+ //#region src/react-feedback-styles.ts
312
+ /**
313
+ * Self-contained styles for the FeedbackButton and FeedbackDialog components.
314
+ * Uses CSS custom properties for theming. Injected via <style> tag with a unique ID
315
+ * to avoid duplication. All selectors are scoped under `[data-reflet-feedback]`.
316
+ */
317
+ const STYLE_ID = "reflet-feedback-styles";
318
+ function injectFeedbackStyles() {
319
+ if (typeof document === "undefined") return;
320
+ if (document.getElementById(STYLE_ID)) return;
321
+ const style = document.createElement("style");
322
+ style.id = STYLE_ID;
323
+ style.textContent = getFeedbackCSS();
324
+ document.head.appendChild(style);
144
325
  }
145
- function useComments(feedbackId, sortBy = "oldest") {
146
- const client = useRefletClient();
147
- const [data, setData] = useState();
148
- const [isLoading, setIsLoading] = useState(!!feedbackId);
149
- const [error, setError] = useState(null);
150
- const fetch = useCallback(async () => {
151
- if (!feedbackId) {
152
- setData(void 0);
153
- setIsLoading(false);
154
- return;
155
- }
156
- setIsLoading(true);
157
- setError(null);
158
- try {
159
- const result = await client.getComments(feedbackId, sortBy);
160
- setData(result);
161
- } catch (err) {
162
- setError(
163
- err instanceof Error ? err : new Error("Failed to fetch comments")
164
- );
165
- } finally {
166
- setIsLoading(false);
167
- }
168
- }, [client, feedbackId, sortBy]);
169
- useEffect(() => {
170
- fetch();
171
- }, [fetch]);
172
- return { data, isLoading, error, refetch: fetch };
326
+ function getFeedbackCSS() {
327
+ return `
328
+ [data-reflet-feedback] {
329
+ --reflet-primary: #6366f1;
330
+ --reflet-primary-hover: #4f46e5;
331
+ --reflet-bg: #ffffff;
332
+ --reflet-bg-secondary: #f9fafb;
333
+ --reflet-text: #111827;
334
+ --reflet-text-secondary: #6b7280;
335
+ --reflet-text-tertiary: #9ca3af;
336
+ --reflet-border: #e5e7eb;
337
+ --reflet-radius: 12px;
338
+ --reflet-radius-sm: 8px;
339
+ --reflet-shadow: 0 20px 60px -12px rgba(0,0,0,0.25), 0 0 0 1px rgba(0,0,0,0.05);
340
+ --reflet-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
341
+ --reflet-success: #22c55e;
342
+ --reflet-error: #ef4444;
173
343
  }
174
- function useRoadmap() {
175
- const client = useRefletClient();
176
- const [data, setData] = useState();
177
- const [isLoading, setIsLoading] = useState(true);
178
- const [error, setError] = useState(null);
179
- const fetch = useCallback(async () => {
180
- setIsLoading(true);
181
- setError(null);
182
- try {
183
- const result = await client.getRoadmap();
184
- setData(result);
185
- } catch (err) {
186
- setError(
187
- err instanceof Error ? err : new Error("Failed to fetch roadmap")
188
- );
189
- } finally {
190
- setIsLoading(false);
191
- }
192
- }, [client]);
193
- useEffect(() => {
194
- fetch();
195
- }, [fetch]);
196
- return { data, isLoading, error, refetch: fetch };
344
+
345
+ @media (prefers-color-scheme: dark) {
346
+ [data-reflet-feedback][data-theme="auto"] {
347
+ --reflet-bg: #1f2937;
348
+ --reflet-bg-secondary: #111827;
349
+ --reflet-text: #f9fafb;
350
+ --reflet-text-secondary: #9ca3af;
351
+ --reflet-text-tertiary: #6b7280;
352
+ --reflet-border: #374151;
353
+ --reflet-shadow: 0 20px 60px -12px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.05);
354
+ }
197
355
  }
198
- function useChangelog(limit) {
199
- const client = useRefletClient();
200
- const [data, setData] = useState();
201
- const [isLoading, setIsLoading] = useState(true);
202
- const [error, setError] = useState(null);
203
- const fetch = useCallback(async () => {
204
- setIsLoading(true);
205
- setError(null);
206
- try {
207
- const result = await client.getChangelog(limit);
208
- setData(result);
209
- } catch (err) {
210
- setError(
211
- err instanceof Error ? err : new Error("Failed to fetch changelog")
212
- );
213
- } finally {
214
- setIsLoading(false);
215
- }
216
- }, [client, limit]);
217
- useEffect(() => {
218
- fetch();
219
- }, [fetch]);
220
- return { data, isLoading, error, refetch: fetch };
356
+
357
+ [data-reflet-feedback][data-theme="dark"] {
358
+ --reflet-bg: #1f2937;
359
+ --reflet-bg-secondary: #111827;
360
+ --reflet-text: #f9fafb;
361
+ --reflet-text-secondary: #9ca3af;
362
+ --reflet-text-tertiary: #6b7280;
363
+ --reflet-border: #374151;
364
+ --reflet-shadow: 0 20px 60px -12px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.05);
365
+ }
366
+
367
+ /* ===== Overlay ===== */
368
+ .reflet-overlay {
369
+ position: fixed;
370
+ inset: 0;
371
+ background: rgba(0,0,0,0.4);
372
+ z-index: 99998;
373
+ animation: reflet-fade-in 150ms ease-out;
374
+ }
375
+
376
+ @keyframes reflet-fade-in {
377
+ from { opacity: 0; }
378
+ to { opacity: 1; }
379
+ }
380
+
381
+ @keyframes reflet-fade-out {
382
+ from { opacity: 1; }
383
+ to { opacity: 0; }
384
+ }
385
+
386
+ @keyframes reflet-slide-up {
387
+ from { transform: translateY(100%); }
388
+ to { transform: translateY(0); }
389
+ }
390
+
391
+ @keyframes reflet-slide-down {
392
+ from { transform: translateY(0); }
393
+ to { transform: translateY(100%); }
394
+ }
395
+
396
+ @keyframes reflet-scale-in {
397
+ from { opacity: 0; transform: translate(-50%, -50%) scale(0.95); }
398
+ to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
399
+ }
400
+
401
+ @keyframes reflet-scale-out {
402
+ from { opacity: 1; transform: translate(-50%, -50%) scale(1); }
403
+ to { opacity: 0; transform: translate(-50%, -50%) scale(0.95); }
221
404
  }
405
+
406
+ @keyframes reflet-checkmark {
407
+ 0% { stroke-dashoffset: 24; }
408
+ 100% { stroke-dashoffset: 0; }
409
+ }
410
+
411
+ @keyframes reflet-spin {
412
+ to { transform: rotate(360deg); }
413
+ }
414
+
415
+ .reflet-overlay[data-closing="true"] {
416
+ animation: reflet-fade-out 150ms ease-in forwards;
417
+ }
418
+
419
+ /* ===== Dialog (Desktop) ===== */
420
+ .reflet-dialog {
421
+ position: fixed;
422
+ top: 50%;
423
+ left: 50%;
424
+ transform: translate(-50%, -50%);
425
+ z-index: 99999;
426
+ background: var(--reflet-bg);
427
+ border-radius: var(--reflet-radius);
428
+ box-shadow: var(--reflet-shadow);
429
+ width: 480px;
430
+ max-width: calc(100vw - 32px);
431
+ max-height: calc(100vh - 64px);
432
+ overflow: hidden;
433
+ font-family: var(--reflet-font);
434
+ color: var(--reflet-text);
435
+ animation: reflet-scale-in 200ms cubic-bezier(0.16, 1, 0.3, 1);
436
+ }
437
+
438
+ .reflet-dialog[data-closing="true"] {
439
+ animation: reflet-scale-out 150ms ease-in forwards;
440
+ }
441
+
442
+ /* ===== Sheet (Mobile) ===== */
443
+ @media (max-width: 640px) {
444
+ .reflet-dialog {
445
+ top: auto;
446
+ left: 0;
447
+ right: 0;
448
+ bottom: 0;
449
+ transform: none;
450
+ width: 100%;
451
+ max-width: 100%;
452
+ border-radius: var(--reflet-radius) var(--reflet-radius) 0 0;
453
+ max-height: 90vh;
454
+ animation: reflet-slide-up 300ms cubic-bezier(0.16, 1, 0.3, 1);
455
+ }
456
+
457
+ .reflet-dialog[data-closing="true"] {
458
+ animation: reflet-slide-down 200ms ease-in forwards;
459
+ }
460
+ }
461
+
462
+ /* ===== Header ===== */
463
+ .reflet-header {
464
+ display: flex;
465
+ align-items: center;
466
+ justify-content: space-between;
467
+ padding: 20px 24px 0;
468
+ }
469
+
470
+ .reflet-title {
471
+ font-size: 18px;
472
+ font-weight: 600;
473
+ margin: 0;
474
+ color: var(--reflet-text);
475
+ line-height: 1.3;
476
+ }
477
+
478
+ .reflet-close {
479
+ display: flex;
480
+ align-items: center;
481
+ justify-content: center;
482
+ width: 32px;
483
+ height: 32px;
484
+ border: none;
485
+ background: transparent;
486
+ border-radius: 8px;
487
+ color: var(--reflet-text-secondary);
488
+ cursor: pointer;
489
+ padding: 0;
490
+ flex-shrink: 0;
491
+ transition: background 150ms, color 150ms;
492
+ }
493
+
494
+ .reflet-close:hover {
495
+ background: var(--reflet-bg-secondary);
496
+ color: var(--reflet-text);
497
+ }
498
+
499
+ .reflet-close svg {
500
+ width: 18px;
501
+ height: 18px;
502
+ }
503
+
504
+ /* ===== Form ===== */
505
+ .reflet-form {
506
+ padding: 20px 24px 24px;
507
+ display: flex;
508
+ flex-direction: column;
509
+ gap: 16px;
510
+ }
511
+
512
+ .reflet-field {
513
+ display: flex;
514
+ flex-direction: column;
515
+ gap: 6px;
516
+ }
517
+
518
+ .reflet-label {
519
+ font-size: 13px;
520
+ font-weight: 500;
521
+ color: var(--reflet-text-secondary);
522
+ }
523
+
524
+ .reflet-input,
525
+ .reflet-textarea {
526
+ width: 100%;
527
+ padding: 10px 12px;
528
+ border: 1px solid var(--reflet-border);
529
+ border-radius: var(--reflet-radius-sm);
530
+ background: var(--reflet-bg);
531
+ color: var(--reflet-text);
532
+ font-family: var(--reflet-font);
533
+ font-size: 14px;
534
+ line-height: 1.5;
535
+ outline: none;
536
+ transition: border-color 150ms, box-shadow 150ms;
537
+ box-sizing: border-box;
538
+ }
539
+
540
+ .reflet-input::placeholder,
541
+ .reflet-textarea::placeholder {
542
+ color: var(--reflet-text-tertiary);
543
+ }
544
+
545
+ .reflet-input:focus,
546
+ .reflet-textarea:focus {
547
+ border-color: var(--reflet-primary);
548
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--reflet-primary) 15%, transparent);
549
+ }
550
+
551
+ .reflet-textarea {
552
+ resize: vertical;
553
+ min-height: 100px;
554
+ }
555
+
556
+ /* ===== Category Selector ===== */
557
+ .reflet-categories {
558
+ display: flex;
559
+ gap: 8px;
560
+ }
561
+
562
+ .reflet-category {
563
+ flex: 1;
564
+ padding: 8px 12px;
565
+ border: 1px solid var(--reflet-border);
566
+ border-radius: var(--reflet-radius-sm);
567
+ background: var(--reflet-bg);
568
+ color: var(--reflet-text-secondary);
569
+ font-family: var(--reflet-font);
570
+ font-size: 13px;
571
+ font-weight: 500;
572
+ cursor: pointer;
573
+ text-align: center;
574
+ transition: all 150ms;
575
+ }
576
+
577
+ .reflet-category:hover {
578
+ border-color: var(--reflet-primary);
579
+ color: var(--reflet-text);
580
+ }
581
+
582
+ .reflet-category[data-selected="true"] {
583
+ border-color: var(--reflet-primary);
584
+ background: color-mix(in srgb, var(--reflet-primary) 8%, transparent);
585
+ color: var(--reflet-primary);
586
+ }
587
+ ${getFeedbackChromeCSS()}
588
+ `;
589
+ }
590
+ //#endregion
591
+ //#region src/react-feedback-dialog-hook.ts
592
+ function useFeedbackDialog({ open, onOpenChange, publicKey: publicKeyProp, baseUrl, user: userProp, userToken: userTokenProp, primaryColor, defaultCategory = "feature", labels: labelsProp, onSubmit, onOpen, onClose }) {
593
+ const context = useContext(RefletContext);
594
+ const publicKey = publicKeyProp ?? context?.publicKey;
595
+ const user = userProp ?? context?.user;
596
+ const userToken = userTokenProp ?? context?.userToken;
597
+ const labels = {
598
+ ...DEFAULT_LABELS,
599
+ ...labelsProp
600
+ };
601
+ const [title, setTitle] = useState("");
602
+ const [description, setDescription] = useState("");
603
+ const [email, setEmail] = useState("");
604
+ const [category, setCategory] = useState(defaultCategory);
605
+ const [honeypot, setHoneypot] = useState("");
606
+ const [isSubmitting, setIsSubmitting] = useState(false);
607
+ const [isSuccess, setIsSuccess] = useState(false);
608
+ const [error, setError] = useState(null);
609
+ const [isClosing, setIsClosing] = useState(false);
610
+ const titleInputRef = useRef(null);
611
+ const dialogRef = useRef(null);
612
+ const previousActiveElement = useRef(null);
613
+ const isAnonymous = !(user || userToken);
614
+ useEffect(() => {
615
+ injectFeedbackStyles();
616
+ }, []);
617
+ useEffect(() => {
618
+ if (!(primaryColor && dialogRef.current)) return;
619
+ const el = dialogRef.current.closest("[data-reflet-feedback]");
620
+ if (el instanceof HTMLElement) el.style.setProperty("--reflet-primary", primaryColor);
621
+ }, [primaryColor]);
622
+ useEffect(() => {
623
+ if (open) {
624
+ previousActiveElement.current = document.activeElement;
625
+ onOpen?.();
626
+ requestAnimationFrame(() => {
627
+ titleInputRef.current?.focus();
628
+ });
629
+ }
630
+ }, [open, onOpen]);
631
+ useEffect(() => {
632
+ if (!open) return;
633
+ const handleKeyDown = (e) => {
634
+ if (e.key === "Escape") {
635
+ e.preventDefault();
636
+ handleClose();
637
+ }
638
+ };
639
+ document.addEventListener("keydown", handleKeyDown);
640
+ return () => document.removeEventListener("keydown", handleKeyDown);
641
+ });
642
+ useEffect(() => {
643
+ if (!open) return;
644
+ const originalOverflow = document.body.style.overflow;
645
+ document.body.style.overflow = "hidden";
646
+ return () => {
647
+ document.body.style.overflow = originalOverflow;
648
+ };
649
+ }, [open]);
650
+ const resetForm = useCallback(() => {
651
+ setTitle("");
652
+ setDescription("");
653
+ setEmail("");
654
+ setCategory(defaultCategory);
655
+ setHoneypot("");
656
+ setError(null);
657
+ setIsSuccess(false);
658
+ setIsSubmitting(false);
659
+ }, [defaultCategory]);
660
+ const handleClose = useCallback(() => {
661
+ setIsClosing(true);
662
+ setTimeout(() => {
663
+ setIsClosing(false);
664
+ onOpenChange(false);
665
+ onClose?.();
666
+ resetForm();
667
+ if (previousActiveElement.current instanceof HTMLElement) previousActiveElement.current.focus();
668
+ }, 200);
669
+ }, [
670
+ onOpenChange,
671
+ onClose,
672
+ resetForm
673
+ ]);
674
+ return {
675
+ category,
676
+ description,
677
+ dialogRef,
678
+ email,
679
+ error,
680
+ handleClose,
681
+ handleSubmit: useCallback(async (e) => {
682
+ e.preventDefault();
683
+ if (honeypot) {
684
+ setIsSuccess(true);
685
+ return;
686
+ }
687
+ if (!publicKey) {
688
+ setError("Missing publicKey. Provide it as a prop or via RefletProvider.");
689
+ return;
690
+ }
691
+ if (!title.trim()) {
692
+ setError(labels.required);
693
+ return;
694
+ }
695
+ setIsSubmitting(true);
696
+ setError(null);
697
+ try {
698
+ const client = new Reflet({
699
+ baseUrl,
700
+ publicKey,
701
+ user,
702
+ userToken
703
+ });
704
+ const categoryPrefix = `[${category.charAt(0).toUpperCase() + category.slice(1)}] `;
705
+ const fullDescription = description.trim() ? description.trim() : "No additional details provided.";
706
+ const result = await client.create({
707
+ description: `${categoryPrefix}${fullDescription}${isAnonymous && email ? `\n\n---\nContact: ${email}` : ""}`,
708
+ title: title.trim()
709
+ });
710
+ setIsSuccess(true);
711
+ onSubmit?.(result);
712
+ setTimeout(() => {
713
+ handleClose();
714
+ }, AUTO_CLOSE_DELAY);
715
+ } catch (err) {
716
+ const message = err instanceof Error ? err.message : "Something went wrong. Please try again.";
717
+ setError(message);
718
+ } finally {
719
+ setIsSubmitting(false);
720
+ }
721
+ }, [
722
+ honeypot,
723
+ publicKey,
724
+ title,
725
+ description,
726
+ email,
727
+ category,
728
+ baseUrl,
729
+ user,
730
+ userToken,
731
+ isAnonymous,
732
+ labels.required,
733
+ onSubmit,
734
+ handleClose
735
+ ]),
736
+ handleTrapFocus: useCallback((e) => {
737
+ if (e.key !== "Tab" || !dialogRef.current) return;
738
+ const focusable = dialogRef.current.querySelectorAll("button, input, textarea, [tabindex]:not([tabindex=\"-1\"])");
739
+ const first = focusable[0];
740
+ const last = focusable[focusable.length - 1];
741
+ if (!(first && last)) return;
742
+ if (e.shiftKey && document.activeElement === first) {
743
+ e.preventDefault();
744
+ last.focus();
745
+ } else if (!e.shiftKey && document.activeElement === last) {
746
+ e.preventDefault();
747
+ first.focus();
748
+ }
749
+ }, []),
750
+ honeypot,
751
+ isAnonymous,
752
+ isClosing,
753
+ isSubmitting,
754
+ isSuccess,
755
+ labels,
756
+ setCategory,
757
+ setDescription,
758
+ setEmail,
759
+ setError,
760
+ setHoneypot,
761
+ setTitle,
762
+ title,
763
+ titleInputRef
764
+ };
765
+ }
766
+ //#endregion
767
+ //#region src/react-feedback-dialog.tsx
768
+ function FeedbackDialog({ open, onOpenChange, publicKey, baseUrl, user, userToken, theme = "auto", primaryColor, defaultCategory = "feature", categories = [
769
+ "feature",
770
+ "bug",
771
+ "question"
772
+ ], labels: labelsProp, onSubmit, onOpen, onClose }) {
773
+ const { labels, title, setTitle, description, setDescription, email, setEmail, category, setCategory, honeypot, setHoneypot, isSubmitting, isSuccess, error, setError, isClosing, isAnonymous, titleInputRef, dialogRef, handleClose, handleSubmit, handleTrapFocus } = useFeedbackDialog({
774
+ baseUrl,
775
+ defaultCategory,
776
+ labels: labelsProp,
777
+ onClose,
778
+ onOpen,
779
+ onOpenChange,
780
+ onSubmit,
781
+ open,
782
+ primaryColor,
783
+ publicKey,
784
+ user,
785
+ userToken
786
+ });
787
+ if (!open) return null;
788
+ return /* @__PURE__ */ jsxs("div", {
789
+ "data-reflet-feedback": "",
790
+ "data-theme": theme,
791
+ children: [/* @__PURE__ */ jsx("button", {
792
+ "aria-label": "Close feedback dialog",
793
+ className: "reflet-overlay",
794
+ "data-closing": isClosing ? "true" : void 0,
795
+ onClick: handleClose,
796
+ tabIndex: 0,
797
+ type: "button"
798
+ }), /* @__PURE__ */ jsx("div", {
799
+ "aria-label": labels.title,
800
+ "aria-modal": "true",
801
+ className: "reflet-dialog",
802
+ "data-closing": isClosing ? "true" : void 0,
803
+ onKeyDown: handleTrapFocus,
804
+ ref: dialogRef,
805
+ role: "dialog",
806
+ children: isSuccess ? /* @__PURE__ */ jsxs("div", {
807
+ className: "reflet-success",
808
+ children: [
809
+ /* @__PURE__ */ jsx("div", {
810
+ className: "reflet-success-icon",
811
+ children: /* @__PURE__ */ jsx("svg", {
812
+ "aria-hidden": "true",
813
+ fill: "none",
814
+ stroke: "currentColor",
815
+ strokeLinecap: "round",
816
+ strokeLinejoin: "round",
817
+ strokeWidth: "2.5",
818
+ viewBox: "0 0 24 24",
819
+ children: /* @__PURE__ */ jsx("path", { d: "M5 13l4 4L19 7" })
820
+ })
821
+ }),
822
+ /* @__PURE__ */ jsx("h2", {
823
+ className: "reflet-success-title",
824
+ children: labels.successTitle
825
+ }),
826
+ /* @__PURE__ */ jsx("p", {
827
+ className: "reflet-success-text",
828
+ children: labels.successMessage
829
+ })
830
+ ]
831
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [
832
+ /* @__PURE__ */ jsxs("div", {
833
+ className: "reflet-header",
834
+ children: [/* @__PURE__ */ jsx("h2", {
835
+ className: "reflet-title",
836
+ children: labels.title
837
+ }), /* @__PURE__ */ jsx("button", {
838
+ "aria-label": "Close",
839
+ className: "reflet-close",
840
+ onClick: handleClose,
841
+ type: "button",
842
+ children: /* @__PURE__ */ jsxs("svg", {
843
+ "aria-hidden": "true",
844
+ fill: "none",
845
+ stroke: "currentColor",
846
+ strokeLinecap: "round",
847
+ strokeLinejoin: "round",
848
+ strokeWidth: "2",
849
+ viewBox: "0 0 24 24",
850
+ children: [/* @__PURE__ */ jsx("line", {
851
+ x1: "18",
852
+ x2: "6",
853
+ y1: "6",
854
+ y2: "18"
855
+ }), /* @__PURE__ */ jsx("line", {
856
+ x1: "6",
857
+ x2: "18",
858
+ y1: "6",
859
+ y2: "18"
860
+ })]
861
+ })
862
+ })]
863
+ }),
864
+ /* @__PURE__ */ jsxs("form", {
865
+ className: "reflet-form",
866
+ onSubmit: handleSubmit,
867
+ children: [
868
+ categories.length > 1 && /* @__PURE__ */ jsx("div", {
869
+ className: "reflet-categories",
870
+ children: categories.map((cat) => /* @__PURE__ */ jsxs("button", {
871
+ className: "reflet-category",
872
+ "data-selected": cat === category ? "true" : void 0,
873
+ onClick: () => setCategory(cat),
874
+ type: "button",
875
+ children: [
876
+ CATEGORY_ICONS[cat],
877
+ " ",
878
+ labels[`category${cat.charAt(0).toUpperCase()}${cat.slice(1)}`]
879
+ ]
880
+ }, cat))
881
+ }),
882
+ /* @__PURE__ */ jsx("div", {
883
+ className: "reflet-field",
884
+ children: /* @__PURE__ */ jsx("input", {
885
+ "aria-label": "Feedback title",
886
+ autoComplete: "off",
887
+ className: "reflet-input",
888
+ maxLength: 100,
889
+ onChange: (e) => {
890
+ setTitle(e.target.value);
891
+ if (error) setError(null);
892
+ },
893
+ placeholder: labels.titlePlaceholder,
894
+ ref: titleInputRef,
895
+ required: true,
896
+ type: "text",
897
+ value: title
898
+ })
899
+ }),
900
+ /* @__PURE__ */ jsx("div", {
901
+ className: "reflet-field",
902
+ children: /* @__PURE__ */ jsx("textarea", {
903
+ "aria-label": "Feedback description",
904
+ className: "reflet-textarea",
905
+ maxLength: 2e3,
906
+ onChange: (e) => setDescription(e.target.value),
907
+ placeholder: labels.descriptionPlaceholder,
908
+ rows: 3,
909
+ value: description
910
+ })
911
+ }),
912
+ isAnonymous && /* @__PURE__ */ jsxs("div", {
913
+ className: "reflet-field",
914
+ children: [/* @__PURE__ */ jsx("label", {
915
+ className: "reflet-label",
916
+ htmlFor: "reflet-email",
917
+ children: labels.emailLabel
918
+ }), /* @__PURE__ */ jsx("input", {
919
+ "aria-label": "Email address",
920
+ autoComplete: "email",
921
+ className: "reflet-input",
922
+ id: "reflet-email",
923
+ onChange: (e) => setEmail(e.target.value),
924
+ placeholder: labels.emailPlaceholder,
925
+ type: "email",
926
+ value: email
927
+ })]
928
+ }),
929
+ /* @__PURE__ */ jsx("div", {
930
+ "aria-hidden": "true",
931
+ className: "reflet-hp",
932
+ children: /* @__PURE__ */ jsx("input", {
933
+ autoComplete: "off",
934
+ name: "website",
935
+ onChange: (e) => setHoneypot(e.target.value),
936
+ tabIndex: -1,
937
+ type: "text",
938
+ value: honeypot
939
+ })
940
+ }),
941
+ error && /* @__PURE__ */ jsx("p", {
942
+ className: "reflet-error-msg",
943
+ children: error
944
+ }),
945
+ /* @__PURE__ */ jsxs("div", {
946
+ className: "reflet-footer",
947
+ children: [/* @__PURE__ */ jsx("button", {
948
+ className: "reflet-btn reflet-btn-secondary",
949
+ onClick: handleClose,
950
+ type: "button",
951
+ children: labels.cancel
952
+ }), /* @__PURE__ */ jsx("button", {
953
+ className: "reflet-btn reflet-btn-primary",
954
+ disabled: isSubmitting || !title.trim(),
955
+ type: "submit",
956
+ children: isSubmitting ? /* @__PURE__ */ jsx("span", { className: "reflet-spinner" }) : labels.submit
957
+ })]
958
+ })
959
+ ]
960
+ }),
961
+ /* @__PURE__ */ jsxs("div", {
962
+ className: "reflet-powered",
963
+ children: [
964
+ "Powered by",
965
+ " ",
966
+ /* @__PURE__ */ jsx("a", {
967
+ href: "https://reflet.app",
968
+ rel: "noopener noreferrer",
969
+ target: "_blank",
970
+ children: "Reflet"
971
+ })
972
+ ]
973
+ })
974
+ ] })
975
+ })]
976
+ });
977
+ }
978
+ //#endregion
979
+ //#region src/react-feedback-button.tsx
980
+ function FeedbackIcon() {
981
+ return /* @__PURE__ */ jsx("svg", {
982
+ "aria-hidden": "true",
983
+ fill: "none",
984
+ height: "16",
985
+ stroke: "currentColor",
986
+ strokeLinecap: "round",
987
+ strokeLinejoin: "round",
988
+ strokeWidth: "2",
989
+ viewBox: "0 0 24 24",
990
+ width: "16",
991
+ children: /* @__PURE__ */ jsx("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" })
992
+ });
993
+ }
994
+ function FeedbackButton({ publicKey, baseUrl, user, userToken, theme = "auto", primaryColor, defaultCategory = "feature", categories, labels, onSubmit, onOpen, onClose, className, asChild = false, children }) {
995
+ const [open, setOpen] = useState(false);
996
+ useState(() => {
997
+ injectFeedbackStyles();
998
+ });
999
+ const handleClick = useCallback(() => {
1000
+ setOpen(true);
1001
+ }, []);
1002
+ let trigger;
1003
+ if (asChild && isValidElement(children)) trigger = cloneElement(children, { onClick: handleClick });
1004
+ else {
1005
+ const triggerLabel = labels?.trigger ?? "Feedback";
1006
+ trigger = /* @__PURE__ */ jsx("div", {
1007
+ "data-reflet-feedback": "",
1008
+ "data-theme": theme,
1009
+ children: /* @__PURE__ */ jsxs("button", {
1010
+ "aria-label": triggerLabel,
1011
+ className: className ?? "reflet-trigger",
1012
+ onClick: handleClick,
1013
+ style: primaryColor ? {
1014
+ "--reflet-primary": primaryColor,
1015
+ "--reflet-primary-hover": primaryColor
1016
+ } : void 0,
1017
+ type: "button",
1018
+ children: [/* @__PURE__ */ jsx(FeedbackIcon, {}), triggerLabel]
1019
+ })
1020
+ });
1021
+ }
1022
+ return /* @__PURE__ */ jsxs(Fragment, { children: [trigger, /* @__PURE__ */ jsx(FeedbackDialog, {
1023
+ baseUrl,
1024
+ categories,
1025
+ defaultCategory,
1026
+ labels,
1027
+ onClose,
1028
+ onOpen,
1029
+ onOpenChange: setOpen,
1030
+ onSubmit,
1031
+ open,
1032
+ primaryColor,
1033
+ publicKey,
1034
+ theme,
1035
+ user,
1036
+ userToken
1037
+ })] });
1038
+ }
1039
+ //#endregion
1040
+ //#region src/react-mutation-hooks.ts
1041
+ function useRefletMutation(mutateFn) {
1042
+ const [isLoading, setIsLoading] = useState(false);
1043
+ const [error, setError] = useState(null);
1044
+ const [data, setData] = useState();
1045
+ return {
1046
+ data,
1047
+ error,
1048
+ isLoading,
1049
+ mutate: useCallback(async (variables) => {
1050
+ setIsLoading(true);
1051
+ setError(null);
1052
+ try {
1053
+ const result = await mutateFn(variables);
1054
+ setData(result);
1055
+ return result;
1056
+ } catch (err) {
1057
+ const wrapped = err instanceof Error ? err : /* @__PURE__ */ new Error("Request failed");
1058
+ setError(wrapped);
1059
+ throw wrapped;
1060
+ } finally {
1061
+ setIsLoading(false);
1062
+ }
1063
+ }, [mutateFn]),
1064
+ reset: useCallback(() => {
1065
+ setData(void 0);
1066
+ setError(null);
1067
+ }, [])
1068
+ };
1069
+ }
1070
+ /**
1071
+ * Hook to create new feedback
1072
+ *
1073
+ * @example
1074
+ * ```tsx
1075
+ * const { mutate: createFeedback, isLoading } = useCreateFeedback();
1076
+ *
1077
+ * const handleSubmit = async (data) => {
1078
+ * const result = await createFeedback(data);
1079
+ * console.log('Created:', result.feedbackId);
1080
+ * };
1081
+ * ```
1082
+ */
222
1083
  function useCreateFeedback() {
223
- const client = useRefletClient();
224
- const [isLoading, setIsLoading] = useState(false);
225
- const [error, setError] = useState(null);
226
- const [data, setData] = useState();
227
- const mutate = useCallback(
228
- async (params) => {
229
- setIsLoading(true);
230
- setError(null);
231
- try {
232
- const result = await client.create(params);
233
- setData(result);
234
- return result;
235
- } catch (err) {
236
- const error2 = err instanceof Error ? err : new Error("Failed to create feedback");
237
- setError(error2);
238
- throw error2;
239
- } finally {
240
- setIsLoading(false);
241
- }
242
- },
243
- [client]
244
- );
245
- const reset = useCallback(() => {
246
- setData(void 0);
247
- setError(null);
248
- }, []);
249
- return { mutate, isLoading, error, data, reset };
1084
+ const client = useRefletClient();
1085
+ return useRefletMutation(useCallback((params) => client.create(params), [client]));
250
1086
  }
1087
+ /**
1088
+ * Hook to vote on feedback
1089
+ *
1090
+ * @example
1091
+ * ```tsx
1092
+ * const { mutate: vote } = useVote();
1093
+ *
1094
+ * <button onClick={() => vote({ feedbackId: 'xxx' })}>
1095
+ * Vote
1096
+ * </button>
1097
+ * ```
1098
+ */
251
1099
  function useVote() {
252
- const client = useRefletClient();
253
- const [isLoading, setIsLoading] = useState(false);
254
- const [error, setError] = useState(null);
255
- const [data, setData] = useState();
256
- const mutate = useCallback(
257
- async ({
258
- feedbackId,
259
- type = "upvote"
260
- }) => {
261
- setIsLoading(true);
262
- setError(null);
263
- try {
264
- const result = await client.vote(feedbackId, type);
265
- setData(result);
266
- return result;
267
- } catch (err) {
268
- const error2 = err instanceof Error ? err : new Error("Failed to vote");
269
- setError(error2);
270
- throw error2;
271
- } finally {
272
- setIsLoading(false);
273
- }
274
- },
275
- [client]
276
- );
277
- const reset = useCallback(() => {
278
- setData(void 0);
279
- setError(null);
280
- }, []);
281
- return { mutate, isLoading, error, data, reset };
1100
+ const client = useRefletClient();
1101
+ return useRefletMutation(useCallback(({ feedbackId, type = "upvote" }) => client.vote(feedbackId, type), [client]));
282
1102
  }
1103
+ /**
1104
+ * Hook to add a comment
1105
+ */
283
1106
  function useAddComment() {
284
- const client = useRefletClient();
285
- const [isLoading, setIsLoading] = useState(false);
286
- const [error, setError] = useState(null);
287
- const [data, setData] = useState();
288
- const mutate = useCallback(
289
- async (params) => {
290
- setIsLoading(true);
291
- setError(null);
292
- try {
293
- const result = await client.comment(params);
294
- setData(result);
295
- return result;
296
- } catch (err) {
297
- const error2 = err instanceof Error ? err : new Error("Failed to add comment");
298
- setError(error2);
299
- throw error2;
300
- } finally {
301
- setIsLoading(false);
302
- }
303
- },
304
- [client]
305
- );
306
- const reset = useCallback(() => {
307
- setData(void 0);
308
- setError(null);
309
- }, []);
310
- return { mutate, isLoading, error, data, reset };
1107
+ const client = useRefletClient();
1108
+ return useRefletMutation(useCallback((params) => client.comment(params), [client]));
311
1109
  }
1110
+ /**
1111
+ * Hook to subscribe/unsubscribe to feedback
1112
+ */
312
1113
  function useSubscription() {
313
- const client = useRefletClient();
314
- const [isLoading, setIsLoading] = useState(false);
315
- const [error, setError] = useState(null);
316
- const subscribe = useCallback(
317
- async (feedbackId) => {
318
- setIsLoading(true);
319
- setError(null);
320
- try {
321
- const result = await client.subscribe(feedbackId);
322
- return result;
323
- } catch (err) {
324
- const error2 = err instanceof Error ? err : new Error("Failed to subscribe");
325
- setError(error2);
326
- throw error2;
327
- } finally {
328
- setIsLoading(false);
329
- }
330
- },
331
- [client]
332
- );
333
- const unsubscribe = useCallback(
334
- async (feedbackId) => {
335
- setIsLoading(true);
336
- setError(null);
337
- try {
338
- const result = await client.unsubscribe(feedbackId);
339
- return result;
340
- } catch (err) {
341
- const error2 = err instanceof Error ? err : new Error("Failed to unsubscribe");
342
- setError(error2);
343
- throw error2;
344
- } finally {
345
- setIsLoading(false);
346
- }
347
- },
348
- [client]
349
- );
350
- return { subscribe, unsubscribe, isLoading, error };
351
- }
352
-
353
- export { RefletProvider, useAddComment, useBoardConfig, useChangelog, useComments, useCreateFeedback, useFeedback, useFeedbackList, useRefletClient, useRefletContext, useRoadmap, useSubscription, useVote };
354
- //# sourceMappingURL=react.js.map
1114
+ const client = useRefletClient();
1115
+ const [isLoading, setIsLoading] = useState(false);
1116
+ const [error, setError] = useState(null);
1117
+ return {
1118
+ error,
1119
+ isLoading,
1120
+ subscribe: useCallback(async (feedbackId) => {
1121
+ setIsLoading(true);
1122
+ setError(null);
1123
+ try {
1124
+ return await client.subscribe(feedbackId);
1125
+ } catch (err) {
1126
+ const wrapped = err instanceof Error ? err : /* @__PURE__ */ new Error("Failed to subscribe");
1127
+ setError(wrapped);
1128
+ throw wrapped;
1129
+ } finally {
1130
+ setIsLoading(false);
1131
+ }
1132
+ }, [client]),
1133
+ unsubscribe: useCallback(async (feedbackId) => {
1134
+ setIsLoading(true);
1135
+ setError(null);
1136
+ try {
1137
+ return await client.unsubscribe(feedbackId);
1138
+ } catch (err) {
1139
+ const wrapped = err instanceof Error ? err : /* @__PURE__ */ new Error("Failed to unsubscribe");
1140
+ setError(wrapped);
1141
+ throw wrapped;
1142
+ } finally {
1143
+ setIsLoading(false);
1144
+ }
1145
+ }, [client])
1146
+ };
1147
+ }
1148
+ //#endregion
1149
+ //#region src/react-query-hooks.ts
1150
+ function useRefletQuery(fetchFn, enabled = true) {
1151
+ const [data, setData] = useState();
1152
+ const [isLoading, setIsLoading] = useState(enabled);
1153
+ const [error, setError] = useState(null);
1154
+ const refetch = useCallback(async () => {
1155
+ setIsLoading(true);
1156
+ setError(null);
1157
+ try {
1158
+ const result = await fetchFn();
1159
+ setData(result);
1160
+ } catch (err) {
1161
+ setError(err instanceof Error ? err : /* @__PURE__ */ new Error("Request failed"));
1162
+ } finally {
1163
+ setIsLoading(false);
1164
+ }
1165
+ }, [fetchFn]);
1166
+ useEffect(() => {
1167
+ if (enabled) refetch();
1168
+ }, [refetch, enabled]);
1169
+ return {
1170
+ data,
1171
+ error,
1172
+ isLoading,
1173
+ refetch
1174
+ };
1175
+ }
1176
+ function useOrganizationConfig() {
1177
+ const client = useRefletClient();
1178
+ return useRefletQuery(useCallback(() => client.getConfig(), [client]));
1179
+ }
1180
+ function useFeedbackList(options = {}) {
1181
+ const client = useRefletClient();
1182
+ const { enabled = true, statusId, status, search, sortBy, limit, offset } = options;
1183
+ const params = useMemo(() => ({
1184
+ limit,
1185
+ offset,
1186
+ search,
1187
+ sortBy,
1188
+ status,
1189
+ statusId
1190
+ }), [
1191
+ statusId,
1192
+ status,
1193
+ search,
1194
+ sortBy,
1195
+ limit,
1196
+ offset
1197
+ ]);
1198
+ return useRefletQuery(useCallback(() => client.list(params), [client, params]), enabled);
1199
+ }
1200
+ function useFeedback(feedbackId) {
1201
+ const client = useRefletClient();
1202
+ return useRefletQuery(useCallback(() => {
1203
+ if (!feedbackId) return Promise.resolve(void 0);
1204
+ return client.get(feedbackId);
1205
+ }, [client, feedbackId]), !!feedbackId);
1206
+ }
1207
+ function useComments(feedbackId, sortBy = "oldest") {
1208
+ const client = useRefletClient();
1209
+ return useRefletQuery(useCallback(() => {
1210
+ if (!feedbackId) return Promise.resolve(void 0);
1211
+ return client.getComments(feedbackId, sortBy);
1212
+ }, [
1213
+ client,
1214
+ feedbackId,
1215
+ sortBy
1216
+ ]), !!feedbackId);
1217
+ }
1218
+ function useRoadmap() {
1219
+ const client = useRefletClient();
1220
+ return useRefletQuery(useCallback(() => client.getRoadmap(), [client]));
1221
+ }
1222
+ function useChangelog(limit) {
1223
+ const client = useRefletClient();
1224
+ return useRefletQuery(useCallback(() => client.getChangelog(limit), [client, limit]));
1225
+ }
1226
+ const CHANGELOG_STORAGE_KEY_PREFIX = "reflet_changelog_seen_";
1227
+ function useUnreadChangelogCount(publicKey) {
1228
+ const { data: entries } = useChangelog();
1229
+ const [unreadCount, setUnreadCount] = useState(0);
1230
+ const storageKey = `${CHANGELOG_STORAGE_KEY_PREFIX}${publicKey}`;
1231
+ useEffect(() => {
1232
+ if (!entries || entries.length === 0) {
1233
+ setUnreadCount(0);
1234
+ return;
1235
+ }
1236
+ let lastSeen = 0;
1237
+ try {
1238
+ const stored = localStorage.getItem(storageKey);
1239
+ lastSeen = stored ? Number(stored) : 0;
1240
+ } catch {}
1241
+ let count = 0;
1242
+ for (const entry of entries) if (entry.publishedAt && entry.publishedAt > lastSeen) count++;
1243
+ setUnreadCount(count);
1244
+ }, [entries, storageKey]);
1245
+ return {
1246
+ markAsRead: useCallback(() => {
1247
+ if (!entries || entries.length === 0) return;
1248
+ let latestTimestamp = 0;
1249
+ for (const entry of entries) if (entry.publishedAt && entry.publishedAt > latestTimestamp) latestTimestamp = entry.publishedAt;
1250
+ if (latestTimestamp > 0) {
1251
+ try {
1252
+ localStorage.setItem(storageKey, String(latestTimestamp));
1253
+ } catch {}
1254
+ setUnreadCount(0);
1255
+ }
1256
+ }, [entries, storageKey]),
1257
+ unreadCount
1258
+ };
1259
+ }
1260
+ //#endregion
1261
+ export { ChangelogWidget, FeedbackButton, FeedbackDialog, Reflet, RefletContext, RefletProvider, useAddComment, useChangelog, useComments, useCreateFeedback, useFeedback, useFeedbackList, useOrganizationConfig, useRefletClient, useRefletContext, useRoadmap, useSubscription, useUnreadChangelogCount, useVote };
1262
+
355
1263
  //# sourceMappingURL=react.js.map