reflet-sdk 0.5.1 → 0.6.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/CHANGELOG.md +22 -0
- package/README.md +24 -4
- package/dist/{client-BWEBWk1k.js → client-DAdWdIFY.js} +17 -13
- package/dist/client-DAdWdIFY.js.map +1 -0
- package/dist/{client-w4Rkl10y.d.ts → client-DF1fdsSC.d.ts} +3 -1
- package/dist/{client-w4Rkl10y.d.ts.map → client-DF1fdsSC.d.ts.map} +1 -1
- package/dist/{devtools-layer-EyK7n9w3.js → devtools-layer-D_0UL_Wd.js} +2879 -2717
- package/dist/devtools-layer-D_0UL_Wd.js.map +1 -0
- package/dist/devtools-next.d.ts +1 -1
- package/dist/devtools-next.d.ts.map +1 -1
- package/dist/devtools-next.js +1 -1
- package/dist/devtools-vite.d.ts +1 -1
- package/dist/devtools-vite.d.ts.map +1 -1
- package/dist/devtools-vite.js +7 -3
- package/dist/devtools-vite.js.map +1 -1
- package/dist/feedback.d.ts.map +1 -1
- package/dist/feedback.js +51 -34
- package/dist/feedback.js.map +1 -1
- package/dist/{handler-CHP0m7KF.js → handler-C9tdKcl0.js} +335 -46
- package/dist/handler-C9tdKcl0.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/{options-7KtQZJj8.d.ts → options-CIBazDPp.d.ts} +3 -1
- package/dist/options-CIBazDPp.d.ts.map +1 -0
- package/dist/{picker-DFfC1uIY.js → picker-C7TkZcBE.js} +60 -44
- package/dist/picker-C7TkZcBE.js.map +1 -0
- package/dist/{protocol-Bn6Bg_0r.js → protocol-ClqTFzwR.js} +4 -1
- package/dist/protocol-ClqTFzwR.js.map +1 -0
- package/dist/react.d.ts +1 -1
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +43 -49
- package/dist/react.js.map +1 -1
- package/dist/{z-index-CKxBgZTG.js → z-index-BL0SGZEP.js} +7 -7
- package/dist/z-index-BL0SGZEP.js.map +1 -0
- package/package.json +3 -2
- package/dist/client-BWEBWk1k.js.map +0 -1
- package/dist/devtools-layer-EyK7n9w3.js.map +0 -1
- package/dist/handler-CHP0m7KF.js.map +0 -1
- package/dist/options-7KtQZJj8.d.ts.map +0 -1
- package/dist/picker-DFfC1uIY.js.map +0 -1
- package/dist/protocol-Bn6Bg_0r.js.map +0 -1
- package/dist/z-index-CKxBgZTG.js.map +0 -1
package/dist/react.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { n as Reflet } from "./client-
|
|
2
|
-
import { a as useRefletContext, i as useRefletClient, n as RefletContext, r as RefletProvider, t as REFLET_Z_INDEX } from "./z-index-
|
|
3
|
-
import { cloneElement, isValidElement, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
1
|
+
import { n as Reflet } from "./client-DAdWdIFY.js";
|
|
2
|
+
import { a as useRefletContext, i as useRefletClient, n as RefletContext, r as RefletProvider, t as REFLET_Z_INDEX } from "./z-index-BL0SGZEP.js";
|
|
3
|
+
import { cloneElement, isValidElement, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
4
4
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
//#region src/react-changelog-widget.tsx
|
|
6
6
|
/**
|
|
@@ -1077,37 +1077,25 @@ function useAddComment() {
|
|
|
1077
1077
|
*/
|
|
1078
1078
|
function useSubscription() {
|
|
1079
1079
|
const client = useRefletClient();
|
|
1080
|
-
const
|
|
1081
|
-
const
|
|
1080
|
+
const subscribeFn = useCallback((feedbackId) => client.subscribe(feedbackId), [client]);
|
|
1081
|
+
const unsubscribeFn = useCallback((feedbackId) => client.unsubscribe(feedbackId), [client]);
|
|
1082
|
+
const subscription = useRefletMutation(subscribeFn);
|
|
1083
|
+
const unsubscription = useRefletMutation(unsubscribeFn);
|
|
1084
|
+
const { mutate: runSubscribe, reset: resetSubscribe } = subscription;
|
|
1085
|
+
const { mutate: runUnsubscribe, reset: resetUnsubscribe } = unsubscription;
|
|
1086
|
+
const subscribe = useCallback((feedbackId) => {
|
|
1087
|
+
resetUnsubscribe();
|
|
1088
|
+
return runSubscribe(feedbackId);
|
|
1089
|
+
}, [resetUnsubscribe, runSubscribe]);
|
|
1090
|
+
const unsubscribe = useCallback((feedbackId) => {
|
|
1091
|
+
resetSubscribe();
|
|
1092
|
+
return runUnsubscribe(feedbackId);
|
|
1093
|
+
}, [resetSubscribe, runUnsubscribe]);
|
|
1082
1094
|
return {
|
|
1083
|
-
error,
|
|
1084
|
-
isLoading,
|
|
1085
|
-
subscribe
|
|
1086
|
-
|
|
1087
|
-
setError(null);
|
|
1088
|
-
try {
|
|
1089
|
-
return await client.subscribe(feedbackId);
|
|
1090
|
-
} catch (err) {
|
|
1091
|
-
const wrapped = err instanceof Error ? err : /* @__PURE__ */ new Error("Failed to subscribe");
|
|
1092
|
-
setError(wrapped);
|
|
1093
|
-
throw wrapped;
|
|
1094
|
-
} finally {
|
|
1095
|
-
setIsLoading(false);
|
|
1096
|
-
}
|
|
1097
|
-
}, [client]),
|
|
1098
|
-
unsubscribe: useCallback(async (feedbackId) => {
|
|
1099
|
-
setIsLoading(true);
|
|
1100
|
-
setError(null);
|
|
1101
|
-
try {
|
|
1102
|
-
return await client.unsubscribe(feedbackId);
|
|
1103
|
-
} catch (err) {
|
|
1104
|
-
const wrapped = err instanceof Error ? err : /* @__PURE__ */ new Error("Failed to unsubscribe");
|
|
1105
|
-
setError(wrapped);
|
|
1106
|
-
throw wrapped;
|
|
1107
|
-
} finally {
|
|
1108
|
-
setIsLoading(false);
|
|
1109
|
-
}
|
|
1110
|
-
}, [client])
|
|
1095
|
+
error: subscription.error ?? unsubscription.error,
|
|
1096
|
+
isLoading: subscription.isLoading || unsubscription.isLoading,
|
|
1097
|
+
subscribe,
|
|
1098
|
+
unsubscribe
|
|
1111
1099
|
};
|
|
1112
1100
|
}
|
|
1113
1101
|
//#endregion
|
|
@@ -1189,24 +1177,27 @@ function useChangelog(limit) {
|
|
|
1189
1177
|
return useRefletQuery(useCallback(() => client.getChangelog(limit), [client, limit]));
|
|
1190
1178
|
}
|
|
1191
1179
|
const CHANGELOG_STORAGE_KEY_PREFIX = "reflet_changelog_seen_";
|
|
1180
|
+
function subscribeToStorage(onStoreChange) {
|
|
1181
|
+
window.addEventListener("storage", onStoreChange);
|
|
1182
|
+
return () => window.removeEventListener("storage", onStoreChange);
|
|
1183
|
+
}
|
|
1184
|
+
function readLastSeen(storageKey) {
|
|
1185
|
+
try {
|
|
1186
|
+
const stored = localStorage.getItem(storageKey);
|
|
1187
|
+
return stored ? Number(stored) : 0;
|
|
1188
|
+
} catch {
|
|
1189
|
+
return 0;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
1192
|
function useUnreadChangelogCount(publicKey) {
|
|
1193
1193
|
const { data: entries } = useChangelog();
|
|
1194
|
-
const [
|
|
1194
|
+
const [readMark, setReadMark] = useState(null);
|
|
1195
1195
|
const storageKey = `${CHANGELOG_STORAGE_KEY_PREFIX}${publicKey}`;
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
let lastSeen = 0;
|
|
1202
|
-
try {
|
|
1203
|
-
const stored = localStorage.getItem(storageKey);
|
|
1204
|
-
lastSeen = stored ? Number(stored) : 0;
|
|
1205
|
-
} catch {}
|
|
1206
|
-
let count = 0;
|
|
1207
|
-
for (const entry of entries) if (entry.publishedAt && entry.publishedAt > lastSeen) count++;
|
|
1208
|
-
setUnreadCount(count);
|
|
1209
|
-
}, [entries, storageKey]);
|
|
1196
|
+
const storedLastSeen = useSyncExternalStore(subscribeToStorage, () => readLastSeen(storageKey), () => 0);
|
|
1197
|
+
const markedLastSeen = readMark?.storageKey === storageKey ? readMark.timestamp : 0;
|
|
1198
|
+
const lastSeen = Math.max(storedLastSeen, markedLastSeen);
|
|
1199
|
+
let unreadCount = 0;
|
|
1200
|
+
for (const entry of entries ?? []) if (entry.publishedAt && entry.publishedAt > lastSeen) unreadCount++;
|
|
1210
1201
|
return {
|
|
1211
1202
|
markAsRead: useCallback(() => {
|
|
1212
1203
|
if (!entries || entries.length === 0) return;
|
|
@@ -1216,7 +1207,10 @@ function useUnreadChangelogCount(publicKey) {
|
|
|
1216
1207
|
try {
|
|
1217
1208
|
localStorage.setItem(storageKey, String(latestTimestamp));
|
|
1218
1209
|
} catch {}
|
|
1219
|
-
|
|
1210
|
+
setReadMark({
|
|
1211
|
+
storageKey,
|
|
1212
|
+
timestamp: latestTimestamp
|
|
1213
|
+
});
|
|
1220
1214
|
}
|
|
1221
1215
|
}, [entries, storageKey]),
|
|
1222
1216
|
unreadCount
|
package/dist/react.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.js","names":[],"sources":["../src/react-changelog-widget.tsx","../src/react-feedback-dialog-types.ts","../src/react-feedback-styles-parts.ts","../src/react-feedback-styles.ts","../src/react-feedback-dialog-hook.ts","../src/react-feedback-dialog.tsx","../src/react-feedback-button.tsx","../src/react-mutation-hooks.ts","../src/react-query-hooks.ts"],"sourcesContent":["import { useCallback, useEffect, useRef } from \"react\";\nimport type { ChangelogEntry } from \"./types\";\n\ninterface ChangelogWidgetProps {\n autoOpenForNew?: boolean;\n locale?: string;\n maxEntries?: number;\n mode?: \"card\" | \"popup\" | \"trigger\";\n onClose?: () => void;\n onEntryClick?: (entry: ChangelogEntry) => void;\n onOpen?: () => void;\n position?: \"bottom-right\" | \"bottom-left\";\n primaryColor?: string;\n publicKey: string;\n theme?: \"light\" | \"dark\" | \"auto\";\n triggerSelector?: string;\n}\n\ninterface WidgetInstance {\n close: () => void;\n destroy: () => void;\n getUnreadCount: () => number;\n init: () => Promise<void>;\n markAsRead: () => void;\n open: () => void;\n}\n\ndeclare global {\n interface Window {\n RefletChangelogWidget?: new (\n config: ChangelogWidgetProps\n ) => WidgetInstance;\n }\n}\n\n/**\n * React component that wraps the Reflet Changelog Widget.\n *\n * Dynamically loads and initializes a RefletChangelogWidget instance.\n *\n * @example\n * ```tsx\n * import { ChangelogWidget } from 'reflet-sdk/react';\n *\n * export function MyApp() {\n * return <ChangelogWidget publicKey=\"fb_pub_xxx\" mode=\"card\" theme=\"auto\" />;\n * }\n * ```\n */\nexport function ChangelogWidget({\n publicKey,\n mode = \"card\",\n position = \"bottom-right\",\n theme = \"light\",\n primaryColor,\n maxEntries,\n triggerSelector,\n autoOpenForNew,\n locale,\n onOpen,\n onClose,\n onEntryClick,\n}: ChangelogWidgetProps) {\n const widgetRef = useRef<WidgetInstance | null>(null);\n\n const initWidget = useCallback(async () => {\n // Check if RefletChangelogWidget is available globally\n const WidgetClass = window.RefletChangelogWidget;\n\n if (WidgetClass) {\n const instance = new WidgetClass({\n autoOpenForNew,\n locale,\n maxEntries,\n mode,\n onClose,\n onEntryClick,\n onOpen,\n position,\n primaryColor,\n publicKey,\n theme,\n triggerSelector,\n });\n\n widgetRef.current = instance;\n await instance.init();\n return;\n }\n\n // Try loading the script dynamically\n const existingScript = document.querySelector(\n 'script[src*=\"reflet-changelog\"]'\n );\n if (!existingScript) {\n const script = document.createElement(\"script\");\n script.src = \"https://cdn.reflet.app/widget/reflet-changelog.v1.js\";\n script.async = true;\n script.onload = () => {\n const LoadedClass = window.RefletChangelogWidget;\n\n if (LoadedClass) {\n const instance = new LoadedClass({\n autoOpenForNew,\n locale,\n maxEntries,\n mode,\n onClose,\n onEntryClick,\n onOpen,\n position,\n primaryColor,\n publicKey,\n theme,\n triggerSelector,\n });\n\n widgetRef.current = instance;\n instance.init();\n }\n };\n document.head.appendChild(script);\n }\n }, [\n publicKey,\n mode,\n position,\n theme,\n primaryColor,\n maxEntries,\n triggerSelector,\n autoOpenForNew,\n locale,\n onOpen,\n onClose,\n onEntryClick,\n ]);\n\n useEffect(() => {\n initWidget();\n\n return () => {\n if (widgetRef.current) {\n widgetRef.current.destroy();\n widgetRef.current = null;\n }\n };\n }, [initWidget]);\n\n // This component doesn't render visible DOM - the widget manages its own Shadow DOM\n return null;\n}\n","import type { CreateFeedbackResponse, RefletUser } from \"./types\";\n\nexport interface FeedbackDialogLabels {\n cancel?: string;\n close?: string;\n descriptionPlaceholder?: string;\n emailLabel?: string;\n emailPlaceholder?: string;\n required?: string;\n submit?: string;\n successMessage?: string;\n successTitle?: string;\n title?: string;\n titlePlaceholder?: string;\n}\n\nexport interface FeedbackDialogProps {\n /** API base URL override */\n baseUrl?: string;\n /** Custom labels / i18n */\n labels?: FeedbackDialogLabels;\n /** Called when dialog closes */\n onClose?: () => void;\n /** Called when dialog opens */\n onOpen?: () => void;\n /** Callback when open state changes */\n onOpenChange: (open: boolean) => void;\n /** Called after successful submission */\n onSubmit?: (result: CreateFeedbackResponse) => void;\n /** Whether the dialog is open */\n open: boolean;\n /** Primary brand color (CSS value) */\n primaryColor?: string;\n /** Public API key (falls back to RefletProvider context) */\n publicKey?: string;\n /** Color theme */\n theme?: \"light\" | \"dark\" | \"auto\";\n /** User identification (falls back to RefletProvider context) */\n user?: RefletUser;\n /** Pre-signed user token */\n userToken?: string;\n}\n\nexport const DEFAULT_LABELS: Required<FeedbackDialogLabels> = {\n cancel: \"Cancel\",\n close: \"Close\",\n descriptionPlaceholder: \"Tell us more... (optional)\",\n emailLabel: \"Email\",\n emailPlaceholder: \"your@email.com (optional)\",\n required: \"Required\",\n submit: \"Send Feedback\",\n successMessage: \"Your feedback has been received. We appreciate your input.\",\n successTitle: \"Thank you!\",\n title: \"Send Feedback\",\n titlePlaceholder: \"What's on your mind?\",\n};\n\nexport const AUTO_CLOSE_DELAY = 2500;\n","export function getFeedbackChromeCSS(): string {\n return `\n.reflet-footer {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n padding-top: 4px;\n}\n\n.reflet-btn {\n min-height: 40px;\n padding: 10px 20px;\n border: none;\n border-radius: var(--reflet-radius-sm);\n font-family: var(--reflet-font);\n font-size: 14px;\n font-weight: 500;\n cursor: pointer;\n transition: background 150ms, color 150ms, opacity 150ms, transform 150ms var(--reflet-ease);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n line-height: 1;\n}\n\n.reflet-btn-primary {\n background: var(--reflet-primary);\n color: var(--reflet-on-primary);\n}\n\n.reflet-btn-primary:hover:not(:disabled) {\n background: var(--reflet-primary-hover);\n}\n\n.reflet-btn-primary:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.reflet-btn-secondary {\n background: transparent;\n color: var(--reflet-text-secondary);\n}\n\n.reflet-btn-secondary:hover {\n background: var(--reflet-bg-secondary);\n color: var(--reflet-text);\n}\n\n.reflet-spinner {\n width: 16px;\n height: 16px;\n border: 2px solid var(--reflet-on-primary-soft);\n border-top-color: var(--reflet-on-primary);\n border-radius: 50%;\n animation: reflet-spin 600ms linear infinite;\n}\n\n.reflet-success {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: 48px 24px;\n text-align: center;\n gap: 16px;\n}\n\n.reflet-success-icon {\n width: 56px;\n height: 56px;\n border-radius: 50%;\n background: color-mix(in srgb, var(--reflet-success) 12%, transparent);\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.reflet-success-icon svg {\n width: 28px;\n height: 28px;\n color: var(--reflet-success);\n}\n\n.reflet-success-icon svg path {\n stroke-dasharray: 24;\n stroke-dashoffset: 24;\n animation: reflet-checkmark 400ms ease-out 100ms forwards;\n}\n\n.reflet-success-title {\n font-size: 18px;\n font-weight: 600;\n color: var(--reflet-text);\n margin: 0;\n}\n\n.reflet-success-text {\n font-size: 14px;\n color: var(--reflet-text-secondary);\n margin: 0;\n}\n\n.reflet-error-msg {\n font-size: 13px;\n color: var(--reflet-error);\n margin: 0;\n}\n\n.reflet-trigger {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n min-height: 40px;\n padding: 10px 18px;\n background: var(--reflet-primary);\n color: var(--reflet-on-primary);\n border: none;\n border-radius: var(--reflet-radius-sm);\n font-family: var(--reflet-font);\n font-size: 14px;\n font-weight: 500;\n cursor: pointer;\n transition: background 150ms, transform 150ms var(--reflet-ease);\n line-height: 1;\n}\n\n.reflet-trigger:hover {\n background: var(--reflet-primary-hover);\n}\n\n.reflet-trigger svg {\n width: 16px;\n height: 16px;\n}\n\n.reflet-hp {\n position: absolute;\n top: -9999px;\n left: -9999px;\n opacity: 0;\n height: 0;\n width: 0;\n z-index: -1;\n}\n\n.reflet-powered {\n text-align: center;\n padding: 0 24px 16px;\n font-size: 11px;\n color: var(--reflet-text-tertiary);\n}\n\n.reflet-powered a {\n color: var(--reflet-text-secondary);\n text-decoration: none;\n font-weight: 500;\n}\n\n.reflet-powered a:hover {\n color: var(--reflet-primary);\n}\n\n@media (max-width: 640px) {\n .reflet-header::before {\n content: '';\n position: absolute;\n top: 8px;\n left: 50%;\n transform: translateX(-50%);\n width: 36px;\n height: 4px;\n border-radius: 2px;\n background: var(--reflet-border);\n }\n\n .reflet-header {\n position: relative;\n padding-top: 24px;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .reflet-success-icon svg path {\n stroke-dasharray: none;\n stroke-dashoffset: 0;\n animation: none;\n }\n}\n`;\n}\n","import { getFeedbackChromeCSS } from \"./react-feedback-styles-parts\";\nimport { REFLET_Z_INDEX } from \"./z-index\";\n\nconst STYLE_ID = \"reflet-feedback-styles\";\n\nexport function injectFeedbackStyles(): void {\n if (typeof document === \"undefined\") {\n return;\n }\n if (document.getElementById(STYLE_ID)) {\n return;\n }\n\n const style = document.createElement(\"style\");\n style.id = STYLE_ID;\n style.textContent = getFeedbackCSS();\n document.head.appendChild(style);\n}\n\nfunction getFeedbackCSS(): string {\n return `\n:root {\n --reflet-scrim: rgb(0 0 0 / 45%);\n}\n\n[data-reflet-feedback] {\n --reflet-primary: #6366f1;\n --reflet-primary-hover: #4f46e5;\n --reflet-on-primary: #ffffff;\n --reflet-on-primary-soft: rgb(255 255 255 / 30%);\n --reflet-bg: #ffffff;\n --reflet-bg-secondary: #f9fafb;\n --reflet-text: #111827;\n --reflet-text-secondary: #6b7280;\n --reflet-text-tertiary: #9ca3af;\n --reflet-border: #e5e7eb;\n --reflet-radius: 12px;\n --reflet-radius-sm: 8px;\n --reflet-shadow: 0 20px 60px -12px rgb(0 0 0 / 25%), 0 0 0 1px rgb(0 0 0 / 5%);\n --reflet-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n --reflet-success: #22c55e;\n --reflet-error: #ef4444;\n --reflet-ease: cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n@media (prefers-color-scheme: dark) {\n [data-reflet-feedback][data-theme=\"auto\"] {\n --reflet-bg: #1f2937;\n --reflet-bg-secondary: #111827;\n --reflet-text: #f9fafb;\n --reflet-text-secondary: #9ca3af;\n --reflet-text-tertiary: #6b7280;\n --reflet-border: #374151;\n --reflet-shadow: 0 20px 60px -12px rgb(0 0 0 / 50%), 0 0 0 1px rgb(255 255 255 / 5%);\n }\n}\n\n[data-reflet-feedback][data-theme=\"dark\"] {\n --reflet-bg: #1f2937;\n --reflet-bg-secondary: #111827;\n --reflet-text: #f9fafb;\n --reflet-text-secondary: #9ca3af;\n --reflet-text-tertiary: #6b7280;\n --reflet-border: #374151;\n --reflet-shadow: 0 20px 60px -12px rgb(0 0 0 / 50%), 0 0 0 1px rgb(255 255 255 / 5%);\n}\n\n@keyframes reflet-fade-in {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n@keyframes reflet-fade-out {\n from { opacity: 1; }\n to { opacity: 0; }\n}\n\n@keyframes reflet-slide-up {\n from { transform: translateY(100%); }\n to { transform: translateY(0); }\n}\n\n@keyframes reflet-slide-down {\n from { transform: translateY(0); }\n to { transform: translateY(100%); }\n}\n\n@keyframes reflet-scale-in {\n from { opacity: 0; transform: translate(-50%, -50%) scale(0.95); }\n to { opacity: 1; transform: translate(-50%, -50%) scale(1); }\n}\n\n@keyframes reflet-scale-out {\n from { opacity: 1; transform: translate(-50%, -50%) scale(1); }\n to { opacity: 0; transform: translate(-50%, -50%) scale(0.95); }\n}\n\n@keyframes reflet-checkmark {\n 0% { stroke-dashoffset: 24; }\n 100% { stroke-dashoffset: 0; }\n}\n\n@keyframes reflet-spin {\n to { transform: rotate(360deg); }\n}\n\n.reflet-dialog {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n z-index: ${REFLET_Z_INDEX.dialogContent};\n margin: 0;\n border: 0;\n padding: 0;\n background: var(--reflet-bg);\n border-radius: var(--reflet-radius);\n box-shadow: var(--reflet-shadow);\n width: 480px;\n max-width: calc(100vw - 32px);\n max-height: calc(100dvh - 64px);\n overflow: hidden;\n overscroll-behavior: contain;\n font-family: var(--reflet-font);\n color: var(--reflet-text);\n animation: reflet-scale-in 200ms var(--reflet-ease);\n}\n\n.reflet-dialog::backdrop {\n z-index: ${REFLET_Z_INDEX.dialogOverlay};\n background: var(--reflet-scrim);\n animation: reflet-fade-in 150ms ease-out;\n}\n\n.reflet-dialog[data-closing=\"true\"] {\n animation: reflet-scale-out 150ms ease-out forwards;\n}\n\n.reflet-dialog[data-closing=\"true\"]::backdrop {\n animation: reflet-fade-out 150ms ease-out forwards;\n}\n\n@media (max-width: 640px) {\n .reflet-dialog {\n top: auto;\n left: 0;\n right: 0;\n bottom: 0;\n transform: none;\n width: 100%;\n max-width: 100%;\n border-radius: var(--reflet-radius) var(--reflet-radius) 0 0;\n max-height: 90dvh;\n padding-bottom: env(safe-area-inset-bottom);\n animation: reflet-slide-up 300ms var(--reflet-ease);\n }\n\n .reflet-dialog[data-closing=\"true\"] {\n animation: reflet-slide-down 200ms ease-out forwards;\n }\n}\n\n.reflet-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 20px 24px 0;\n}\n\n.reflet-title {\n font-size: 18px;\n font-weight: 600;\n margin: 0;\n color: var(--reflet-text);\n line-height: 1.3;\n}\n\n.reflet-close {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 40px;\n height: 40px;\n border: none;\n background: transparent;\n border-radius: 8px;\n color: var(--reflet-text-secondary);\n cursor: pointer;\n padding: 0;\n flex-shrink: 0;\n transition: background 150ms, color 150ms, transform 150ms var(--reflet-ease);\n}\n\n.reflet-close:hover {\n background: var(--reflet-bg-secondary);\n color: var(--reflet-text);\n}\n\n.reflet-close svg {\n width: 18px;\n height: 18px;\n}\n\n.reflet-form {\n padding: 20px 24px 24px;\n display: flex;\n flex-direction: column;\n gap: 16px;\n}\n\n.reflet-field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.reflet-label {\n font-size: 13px;\n font-weight: 500;\n color: var(--reflet-text-secondary);\n}\n\n.reflet-input,\n.reflet-textarea {\n width: 100%;\n padding: 10px 12px;\n border: 1px solid var(--reflet-border);\n border-radius: var(--reflet-radius-sm);\n background: var(--reflet-bg);\n color: var(--reflet-text);\n font-family: var(--reflet-font);\n font-size: 14px;\n line-height: 1.5;\n outline: none;\n transition: border-color 150ms, box-shadow 150ms;\n box-sizing: border-box;\n}\n\n.reflet-input::placeholder,\n.reflet-textarea::placeholder {\n color: var(--reflet-text-tertiary);\n}\n\n.reflet-input:focus,\n.reflet-textarea:focus {\n border-color: var(--reflet-primary);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--reflet-primary) 15%, transparent);\n}\n\n.reflet-textarea {\n resize: vertical;\n min-height: 100px;\n}\n\n[data-reflet-feedback] :focus-visible {\n outline: 2px solid var(--reflet-primary);\n outline-offset: 2px;\n}\n\n[data-reflet-feedback] button:active:not(:disabled) {\n transform: scale(0.97);\n}\n\n@media (pointer: coarse), (max-width: 640px) {\n .reflet-input,\n .reflet-textarea {\n font-size: 16px;\n }\n}\n${getFeedbackChromeCSS()}\n@media (prefers-reduced-motion: reduce) {\n [data-reflet-feedback] *,\n [data-reflet-feedback] *::before,\n [data-reflet-feedback] *::after,\n .reflet-dialog,\n .reflet-dialog::backdrop {\n animation-duration: 1ms !important;\n animation-iteration-count: 1 !important;\n transition-duration: 1ms !important;\n }\n\n [data-reflet-feedback] button:active:not(:disabled) {\n transform: none;\n }\n}\n`;\n}\n","import {\n type FormEvent,\n type RefObject,\n useContext,\n useEffect,\n useRef,\n useState,\n} from \"react\";\nimport { Reflet } from \"./client\";\nimport { RefletContext } from \"./react-context\";\nimport {\n AUTO_CLOSE_DELAY,\n DEFAULT_LABELS,\n type FeedbackDialogProps,\n} from \"./react-feedback-dialog-types\";\nimport { injectFeedbackStyles } from \"./react-feedback-styles\";\n\nconst CLOSE_ANIMATION_MS = 200;\n\nfunction openDialog(dialog: HTMLDialogElement) {\n if (typeof dialog.showModal === \"function\") {\n dialog.showModal();\n return;\n }\n dialog.open = true;\n}\n\nfunction closeDialog(dialog: HTMLDialogElement) {\n if (typeof dialog.close === \"function\") {\n dialog.close();\n return;\n }\n dialog.open = false;\n}\n\nfunction useModalDialog(\n open: boolean,\n dialogRef: RefObject<HTMLDialogElement | null>,\n titleInputRef: RefObject<HTMLInputElement | null>\n) {\n useEffect(() => {\n const dialog = dialogRef.current;\n if (!(open && dialog) || dialog.open) {\n return;\n }\n openDialog(dialog);\n titleInputRef.current?.focus();\n return () => closeDialog(dialog);\n }, [open, dialogRef, titleInputRef]);\n}\n\nfunction useBackdropDismiss(\n open: boolean,\n dialogRef: RefObject<HTMLDialogElement | null>,\n onDismiss: () => void\n) {\n useEffect(() => {\n const dialog = dialogRef.current;\n if (!(open && dialog)) {\n return;\n }\n const dismiss = (event: MouseEvent) => {\n if (event.target === dialog) {\n onDismiss();\n }\n };\n dialog.addEventListener(\"click\", dismiss);\n return () => dialog.removeEventListener(\"click\", dismiss);\n }, [open, dialogRef, onDismiss]);\n}\n\nfunction useScrollLock(open: boolean) {\n useEffect(() => {\n if (!open) {\n return;\n }\n const root = document.documentElement;\n const previousOverflow = root.style.overflow;\n const previousGutter = root.style.getPropertyValue(\"scrollbar-gutter\");\n root.style.setProperty(\"scrollbar-gutter\", \"stable\");\n root.style.overflow = \"hidden\";\n return () => {\n root.style.overflow = previousOverflow;\n if (previousGutter) {\n root.style.setProperty(\"scrollbar-gutter\", previousGutter);\n } else {\n root.style.removeProperty(\"scrollbar-gutter\");\n }\n };\n }, [open]);\n}\n\nfunction usePrimaryColor(\n primaryColor: string | undefined,\n dialogRef: RefObject<HTMLDialogElement | null>\n) {\n useEffect(() => {\n injectFeedbackStyles();\n }, []);\n\n useEffect(() => {\n if (!(primaryColor && dialogRef.current)) {\n return;\n }\n const host = dialogRef.current.closest(\"[data-reflet-feedback]\");\n if (host instanceof HTMLElement) {\n host.style.setProperty(\"--reflet-primary\", primaryColor);\n }\n }, [primaryColor, dialogRef]);\n}\n\nexport function useFeedbackDialog({\n open,\n onOpenChange,\n publicKey: publicKeyProp,\n baseUrl,\n user: userProp,\n userToken: userTokenProp,\n primaryColor,\n labels: labelsProp,\n onSubmit,\n onOpen,\n onClose,\n}: Omit<FeedbackDialogProps, \"theme\">) {\n const context = useContext(RefletContext);\n const publicKey = publicKeyProp ?? context?.publicKey;\n const user = userProp ?? context?.user;\n const userToken = userTokenProp ?? context?.userToken;\n\n const labels = { ...DEFAULT_LABELS, ...labelsProp };\n\n const [title, setTitle] = useState(\"\");\n const [description, setDescription] = useState(\"\");\n const [email, setEmail] = useState(\"\");\n const [honeypot, setHoneypot] = useState(\"\");\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [isSuccess, setIsSuccess] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [isClosing, setIsClosing] = useState(false);\n\n const titleInputRef = useRef<HTMLInputElement>(null);\n const dialogRef = useRef<HTMLDialogElement>(null);\n\n const isAnonymous = !(user || userToken);\n\n usePrimaryColor(primaryColor, dialogRef);\n useModalDialog(open, dialogRef, titleInputRef);\n useScrollLock(open);\n\n useEffect(() => {\n if (open) {\n onOpen?.();\n }\n }, [open, onOpen]);\n\n const resetForm = () => {\n setTitle(\"\");\n setDescription(\"\");\n setEmail(\"\");\n setHoneypot(\"\");\n setError(null);\n setIsSuccess(false);\n setIsSubmitting(false);\n };\n\n const handleClose = () => {\n setIsClosing(true);\n setTimeout(() => {\n setIsClosing(false);\n onOpenChange(false);\n onClose?.();\n resetForm();\n }, CLOSE_ANIMATION_MS);\n };\n\n useBackdropDismiss(open, dialogRef, handleClose);\n\n const buildDescription = () => {\n const body = description.trim() || \"No additional details provided.\";\n const contact = isAnonymous && email ? `\\n\\n---\\nContact: ${email}` : \"\";\n return `${body}${contact}`;\n };\n\n const handleSubmit = async (event: FormEvent) => {\n event.preventDefault();\n\n if (honeypot) {\n setIsSuccess(true);\n return;\n }\n\n if (!publicKey) {\n setError(\n \"Missing publicKey. Provide it as a prop or via RefletProvider.\"\n );\n return;\n }\n\n if (!title.trim()) {\n setError(labels.required);\n return;\n }\n\n setIsSubmitting(true);\n setError(null);\n\n try {\n const client = new Reflet({ baseUrl, publicKey, user, userToken });\n const result = await client.create({\n description: buildDescription(),\n title: title.trim(),\n });\n\n setIsSuccess(true);\n onSubmit?.(result);\n setTimeout(handleClose, AUTO_CLOSE_DELAY);\n } catch (err) {\n setError(\n err instanceof Error\n ? err.message\n : \"Something went wrong. Please try again.\"\n );\n } finally {\n setIsSubmitting(false);\n }\n };\n\n return {\n description,\n dialogRef,\n email,\n error,\n handleClose,\n handleSubmit,\n honeypot,\n isAnonymous,\n isClosing,\n isSubmitting,\n isSuccess,\n labels,\n setDescription,\n setEmail,\n setError,\n setHoneypot,\n setTitle,\n title,\n titleInputRef,\n };\n}\n","import { useFeedbackDialog } from \"./react-feedback-dialog-hook\";\nimport type { FeedbackDialogProps } from \"./react-feedback-dialog-types\";\n\nexport type {\n FeedbackDialogLabels,\n FeedbackDialogProps,\n} from \"./react-feedback-dialog-types\";\n\nexport function FeedbackDialog({\n open,\n onOpenChange,\n publicKey,\n baseUrl,\n user,\n userToken,\n theme = \"auto\",\n primaryColor,\n labels: labelsProp,\n onSubmit,\n onOpen,\n onClose,\n}: FeedbackDialogProps) {\n const {\n labels,\n title,\n setTitle,\n description,\n setDescription,\n email,\n setEmail,\n honeypot,\n setHoneypot,\n isSubmitting,\n isSuccess,\n error,\n setError,\n isClosing,\n isAnonymous,\n titleInputRef,\n dialogRef,\n handleClose,\n handleSubmit,\n } = useFeedbackDialog({\n baseUrl,\n labels: labelsProp,\n onClose,\n onOpen,\n onOpenChange,\n onSubmit,\n open,\n primaryColor,\n publicKey,\n user,\n userToken,\n });\n\n if (!open) {\n return null;\n }\n\n return (\n <div data-reflet-feedback=\"\" data-theme={theme}>\n <dialog\n aria-label={labels.title}\n className=\"reflet-dialog\"\n data-closing={isClosing ? \"true\" : undefined}\n onCancel={(event) => {\n event.preventDefault();\n handleClose();\n }}\n ref={dialogRef}\n >\n {isSuccess ? (\n <div className=\"reflet-success\">\n <div className=\"reflet-success-icon\">\n <svg\n aria-hidden=\"true\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth=\"2.5\"\n viewBox=\"0 0 24 24\"\n >\n <path d=\"M5 13l4 4L19 7\" />\n </svg>\n </div>\n <h2 className=\"reflet-success-title\">{labels.successTitle}</h2>\n <p className=\"reflet-success-text\">{labels.successMessage}</p>\n </div>\n ) : (\n <>\n <div className=\"reflet-header\">\n <h2 className=\"reflet-title\">{labels.title}</h2>\n <button\n aria-label={labels.close}\n className=\"reflet-close\"\n onClick={handleClose}\n type=\"button\"\n >\n <svg\n aria-hidden=\"true\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth=\"2\"\n viewBox=\"0 0 24 24\"\n >\n <line x1=\"18\" x2=\"6\" y1=\"6\" y2=\"18\" />\n <line x1=\"6\" x2=\"18\" y1=\"6\" y2=\"18\" />\n </svg>\n </button>\n </div>\n\n <form className=\"reflet-form\" onSubmit={handleSubmit}>\n <div className=\"reflet-field\">\n <input\n aria-label=\"Feedback title\"\n autoComplete=\"off\"\n className=\"reflet-input\"\n maxLength={100}\n onChange={(e) => {\n setTitle(e.target.value);\n if (error) {\n setError(null);\n }\n }}\n placeholder={labels.titlePlaceholder}\n ref={titleInputRef}\n required\n type=\"text\"\n value={title}\n />\n </div>\n\n <div className=\"reflet-field\">\n <textarea\n aria-label=\"Feedback description\"\n className=\"reflet-textarea\"\n maxLength={2000}\n onChange={(e) => setDescription(e.target.value)}\n placeholder={labels.descriptionPlaceholder}\n rows={3}\n value={description}\n />\n </div>\n\n {isAnonymous && (\n <div className=\"reflet-field\">\n <label className=\"reflet-label\" htmlFor=\"reflet-email\">\n {labels.emailLabel}\n </label>\n <input\n autoComplete=\"email\"\n className=\"reflet-input\"\n id=\"reflet-email\"\n onChange={(e) => setEmail(e.target.value)}\n placeholder={labels.emailPlaceholder}\n type=\"email\"\n value={email}\n />\n </div>\n )}\n\n <div aria-hidden=\"true\" className=\"reflet-hp\">\n <input\n autoComplete=\"off\"\n name=\"website\"\n onChange={(e) => setHoneypot(e.target.value)}\n tabIndex={-1}\n type=\"text\"\n value={honeypot}\n />\n </div>\n\n {error && <p className=\"reflet-error-msg\">{error}</p>}\n\n <div className=\"reflet-footer\">\n <button\n className=\"reflet-btn reflet-btn-secondary\"\n onClick={handleClose}\n type=\"button\"\n >\n {labels.cancel}\n </button>\n <button\n className=\"reflet-btn reflet-btn-primary\"\n disabled={isSubmitting || !title.trim()}\n type=\"submit\"\n >\n {isSubmitting ? (\n <span className=\"reflet-spinner\" />\n ) : (\n labels.submit\n )}\n </button>\n </div>\n </form>\n\n <div className=\"reflet-powered\">\n Powered by{\" \"}\n <a\n href=\"https://reflet.app\"\n rel=\"noopener noreferrer\"\n target=\"_blank\"\n >\n Reflet\n </a>\n </div>\n </>\n )}\n </dialog>\n </div>\n );\n}\n","import {\n cloneElement,\n isValidElement,\n type ReactElement,\n type ReactNode,\n useCallback,\n useState,\n} from \"react\";\nimport {\n FeedbackDialog,\n type FeedbackDialogLabels,\n} from \"./react-feedback-dialog\";\nimport { injectFeedbackStyles } from \"./react-feedback-styles\";\nimport type { CreateFeedbackResponse, RefletUser } from \"./types\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface FeedbackButtonProps {\n /**\n * Render as child element instead of the default button.\n * When true, the first child element receives the onClick handler.\n */\n asChild?: boolean;\n /** API base URL override */\n baseUrl?: string;\n /** Custom trigger content or element (used with asChild) */\n children?: ReactNode;\n /** Custom CSS class for the default trigger button */\n className?: string;\n /** Custom labels / i18n */\n labels?: FeedbackDialogLabels & { trigger?: string };\n /** Called when dialog closes */\n onClose?: () => void;\n /** Called when dialog opens */\n onOpen?: () => void;\n /** Called after successful submission */\n onSubmit?: (result: CreateFeedbackResponse) => void;\n /** Primary brand color (CSS value) */\n primaryColor?: string;\n /** Public API key (falls back to RefletProvider context) */\n publicKey?: string;\n /** Color theme */\n theme?: \"light\" | \"dark\" | \"auto\";\n /** User identification (falls back to RefletProvider context) */\n user?: RefletUser;\n /** Pre-signed user token */\n userToken?: string;\n}\n\n// ============================================\n// Feedback Icon SVG\n// ============================================\n\nfunction FeedbackIcon() {\n return (\n <svg\n aria-hidden=\"true\"\n fill=\"none\"\n height=\"16\"\n stroke=\"currentColor\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth=\"2\"\n viewBox=\"0 0 24 24\"\n width=\"16\"\n >\n <path d=\"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\" />\n </svg>\n );\n}\n\n// ============================================\n// Component\n// ============================================\n\nexport function FeedbackButton({\n publicKey,\n baseUrl,\n user,\n userToken,\n theme = \"auto\",\n primaryColor,\n labels,\n onSubmit,\n onOpen,\n onClose,\n className,\n asChild = false,\n children,\n}: FeedbackButtonProps) {\n const [open, setOpen] = useState(false);\n\n // Inject styles so the trigger button is styled\n useState(() => {\n injectFeedbackStyles();\n });\n\n const handleClick = useCallback(() => {\n setOpen(true);\n }, []);\n\n // Render trigger\n let trigger: ReactNode;\n\n if (asChild && isValidElement(children)) {\n trigger = cloneElement(children as ReactElement<{ onClick?: () => void }>, {\n onClick: handleClick,\n });\n } else {\n const triggerLabel = labels?.trigger ?? \"Feedback\";\n trigger = (\n <div data-reflet-feedback=\"\" data-theme={theme}>\n <button\n aria-label={triggerLabel}\n className={className ?? \"reflet-trigger\"}\n onClick={handleClick}\n style={\n primaryColor\n ? ({\n \"--reflet-primary\": primaryColor,\n \"--reflet-primary-hover\": primaryColor,\n } as React.CSSProperties)\n : undefined\n }\n type=\"button\"\n >\n <FeedbackIcon />\n {triggerLabel}\n </button>\n </div>\n );\n }\n\n return (\n <>\n {trigger}\n <FeedbackDialog\n baseUrl={baseUrl}\n labels={labels}\n onClose={onClose}\n onOpen={onOpen}\n onOpenChange={setOpen}\n onSubmit={onSubmit}\n open={open}\n primaryColor={primaryColor}\n publicKey={publicKey}\n theme={theme}\n user={user}\n userToken={userToken}\n />\n </>\n );\n}\n","import { useCallback, useState } from \"react\";\nimport { useRefletClient } from \"./react-context\";\nimport type { UseMutationResult } from \"./react-hooks-types\";\nimport type { AddCommentParams, CreateFeedbackParams } from \"./types\";\n\n// ============================================\n// Generic mutation hook — single source of truth\n// ============================================\n\nfunction useRefletMutation<TData, TVariables>(\n mutateFn: (variables: TVariables) => Promise<TData>\n): UseMutationResult<TData, TVariables> {\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n const [data, setData] = useState<TData | undefined>();\n\n const mutate = useCallback(\n async (variables: TVariables) => {\n setIsLoading(true);\n setError(null);\n try {\n const result = await mutateFn(variables);\n setData(result);\n return result;\n } catch (err) {\n const wrapped =\n err instanceof Error ? err : new Error(\"Request failed\");\n setError(wrapped);\n throw wrapped;\n } finally {\n setIsLoading(false);\n }\n },\n [mutateFn]\n );\n\n const reset = useCallback(() => {\n setData(undefined);\n setError(null);\n }, []);\n\n return { data, error, isLoading, mutate, reset };\n}\n\n// ============================================\n// Create Feedback Mutation Hook\n// ============================================\n\n/**\n * Hook to create new feedback\n *\n * @example\n * ```tsx\n * const { mutate: createFeedback, isLoading } = useCreateFeedback();\n *\n * const handleSubmit = async (data) => {\n * const result = await createFeedback(data);\n * console.log('Created:', result.feedbackId);\n * };\n * ```\n */\nexport function useCreateFeedback(): UseMutationResult<\n { feedbackId: string; isApproved: boolean },\n CreateFeedbackParams\n> {\n const client = useRefletClient();\n const mutateFn = useCallback(\n (params: CreateFeedbackParams) => client.create(params),\n [client]\n );\n return useRefletMutation(mutateFn);\n}\n\n// ============================================\n// Vote Mutation Hook\n// ============================================\n\n/**\n * Hook to vote on feedback\n *\n * @example\n * ```tsx\n * const { mutate: vote } = useVote();\n *\n * <button onClick={() => vote({ feedbackId: 'xxx' })}>\n * Vote\n * </button>\n * ```\n */\nexport function useVote(): UseMutationResult<\n { voted: boolean; voteCount: number },\n { feedbackId: string; type?: \"upvote\" | \"downvote\" }\n> {\n const client = useRefletClient();\n const mutateFn = useCallback(\n ({\n feedbackId,\n type = \"upvote\",\n }: {\n feedbackId: string;\n type?: \"upvote\" | \"downvote\";\n }) => client.vote(feedbackId, type),\n [client]\n );\n return useRefletMutation(mutateFn);\n}\n\n// ============================================\n// Comment Mutation Hook\n// ============================================\n\n/**\n * Hook to add a comment\n */\nexport function useAddComment(): UseMutationResult<\n { commentId: string },\n AddCommentParams\n> {\n const client = useRefletClient();\n const mutateFn = useCallback(\n (params: AddCommentParams) => client.comment(params),\n [client]\n );\n return useRefletMutation(mutateFn);\n}\n\n// ============================================\n// Subscribe Mutation Hook\n// ============================================\n\n/**\n * Hook to subscribe/unsubscribe to feedback\n */\nexport function useSubscription(): {\n subscribe: (feedbackId: string) => Promise<{ subscribed: boolean }>;\n unsubscribe: (feedbackId: string) => Promise<{ unsubscribed: boolean }>;\n isLoading: boolean;\n error: Error | null;\n} {\n const client = useRefletClient();\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const subscribe = useCallback(\n async (feedbackId: string) => {\n setIsLoading(true);\n setError(null);\n try {\n return await client.subscribe(feedbackId);\n } catch (err) {\n const wrapped =\n err instanceof Error ? err : new Error(\"Failed to subscribe\");\n setError(wrapped);\n throw wrapped;\n } finally {\n setIsLoading(false);\n }\n },\n [client]\n );\n\n const unsubscribe = useCallback(\n async (feedbackId: string) => {\n setIsLoading(true);\n setError(null);\n try {\n return await client.unsubscribe(feedbackId);\n } catch (err) {\n const wrapped =\n err instanceof Error ? err : new Error(\"Failed to unsubscribe\");\n setError(wrapped);\n throw wrapped;\n } finally {\n setIsLoading(false);\n }\n },\n [client]\n );\n\n return { error, isLoading, subscribe, unsubscribe };\n}\n","import { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { useRefletClient } from \"./react-context\";\nimport type { UseQueryResult } from \"./react-hooks-types\";\nimport type {\n ChangelogEntry,\n Comment,\n FeedbackDetail,\n FeedbackItem,\n FeedbackListParams,\n OrganizationConfig,\n Roadmap,\n} from \"./types\";\n\n// ============================================\n// Generic query hook — single source of truth\n// ============================================\n\nfunction useRefletQuery<T>(\n fetchFn: () => Promise<T>,\n enabled = true\n): UseQueryResult<T> {\n const [data, setData] = useState<T | undefined>();\n const [isLoading, setIsLoading] = useState(enabled);\n const [error, setError] = useState<Error | null>(null);\n\n const refetch = useCallback(async () => {\n setIsLoading(true);\n setError(null);\n try {\n const result = await fetchFn();\n setData(result);\n } catch (err) {\n setError(err instanceof Error ? err : new Error(\"Request failed\"));\n } finally {\n setIsLoading(false);\n }\n }, [fetchFn]);\n\n useEffect(() => {\n if (enabled) {\n refetch();\n }\n }, [refetch, enabled]);\n\n return { data, error, isLoading, refetch };\n}\n\n// ============================================\n// Organization Config Hook\n// ============================================\n\nexport function useOrganizationConfig(): UseQueryResult<OrganizationConfig> {\n const client = useRefletClient();\n const fetchFn = useCallback(() => client.getConfig(), [client]);\n return useRefletQuery(fetchFn);\n}\n\n// ============================================\n// Feedback List Hook\n// ============================================\n\nexport interface UseFeedbackListOptions extends FeedbackListParams {\n enabled?: boolean;\n}\n\nexport function useFeedbackList(\n options: UseFeedbackListOptions = {}\n): UseQueryResult<{ items: FeedbackItem[]; total: number; hasMore: boolean }> {\n const client = useRefletClient();\n const {\n enabled = true,\n statusId,\n status,\n search,\n sortBy,\n limit,\n offset,\n } = options;\n\n const params = useMemo(\n () => ({ limit, offset, search, sortBy, status, statusId }),\n [statusId, status, search, sortBy, limit, offset]\n );\n\n const fetchFn = useCallback(() => client.list(params), [client, params]);\n return useRefletQuery(fetchFn, enabled);\n}\n\n// ============================================\n// Single Feedback Hook\n// ============================================\n\nexport function useFeedback(\n feedbackId: string | undefined\n): UseQueryResult<FeedbackDetail> {\n const client = useRefletClient();\n\n const fetchFn = useCallback(() => {\n if (!feedbackId) {\n return Promise.resolve(undefined as unknown as FeedbackDetail);\n }\n return client.get(feedbackId);\n }, [client, feedbackId]);\n\n return useRefletQuery(fetchFn, !!feedbackId);\n}\n\n// ============================================\n// Comments Hook\n// ============================================\n\nexport function useComments(\n feedbackId: string | undefined,\n sortBy: \"newest\" | \"oldest\" = \"oldest\"\n): UseQueryResult<Comment[]> {\n const client = useRefletClient();\n\n const fetchFn = useCallback(() => {\n if (!feedbackId) {\n return Promise.resolve(undefined as unknown as Comment[]);\n }\n return client.getComments(feedbackId, sortBy);\n }, [client, feedbackId, sortBy]);\n\n return useRefletQuery(fetchFn, !!feedbackId);\n}\n\n// ============================================\n// Roadmap Hook\n// ============================================\n\nexport function useRoadmap(): UseQueryResult<Roadmap> {\n const client = useRefletClient();\n const fetchFn = useCallback(() => client.getRoadmap(), [client]);\n return useRefletQuery(fetchFn);\n}\n\n// ============================================\n// Changelog Hook\n// ============================================\n\nexport function useChangelog(limit?: number): UseQueryResult<ChangelogEntry[]> {\n const client = useRefletClient();\n const fetchFn = useCallback(\n () => client.getChangelog(limit),\n [client, limit]\n );\n return useRefletQuery(fetchFn);\n}\n\n// ============================================\n// Unread Changelog Count Hook\n// ============================================\n\nconst CHANGELOG_STORAGE_KEY_PREFIX = \"reflet_changelog_seen_\";\n\nexport function useUnreadChangelogCount(publicKey: string): {\n unreadCount: number;\n markAsRead: () => void;\n} {\n const { data: entries } = useChangelog();\n const [unreadCount, setUnreadCount] = useState(0);\n\n const storageKey = `${CHANGELOG_STORAGE_KEY_PREFIX}${publicKey}`;\n\n useEffect(() => {\n if (!entries || entries.length === 0) {\n setUnreadCount(0);\n return;\n }\n\n let lastSeen = 0;\n try {\n const stored = localStorage.getItem(storageKey);\n lastSeen = stored ? Number(stored) : 0;\n } catch {\n // localStorage unavailable\n }\n\n let count = 0;\n for (const entry of entries) {\n if (entry.publishedAt && entry.publishedAt > lastSeen) {\n count++;\n }\n }\n setUnreadCount(count);\n }, [entries, storageKey]);\n\n const markAsRead = useCallback(() => {\n if (!entries || entries.length === 0) {\n return;\n }\n\n let latestTimestamp = 0;\n for (const entry of entries) {\n if (entry.publishedAt && entry.publishedAt > latestTimestamp) {\n latestTimestamp = entry.publishedAt;\n }\n }\n\n if (latestTimestamp > 0) {\n try {\n localStorage.setItem(storageKey, String(latestTimestamp));\n } catch {\n // localStorage unavailable\n }\n setUnreadCount(0);\n }\n }, [entries, storageKey]);\n\n return { markAsRead, unreadCount };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAiDA,SAAgB,gBAAgB,EAC9B,WACA,OAAO,QACP,WAAW,gBACX,QAAQ,SACR,cACA,YACA,iBACA,gBACA,QACA,QACA,SACA,gBACuB;CACvB,MAAM,YAAY,OAA8B,IAAI;CAEpD,MAAM,aAAa,YAAY,YAAY;EAEzC,MAAM,cAAc,OAAO;EAE3B,IAAI,aAAa;GACf,MAAM,WAAW,IAAI,YAAY;IAC/B;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;GACF,CAAC;GAED,UAAU,UAAU;GACpB,MAAM,SAAS,KAAK;GACpB;EACF;EAMA,IAAI,CAHmB,SAAS,cAC9B,mCAEgB,GAAG;GACnB,MAAM,SAAS,SAAS,cAAc,QAAQ;GAC9C,OAAO,MAAM;GACb,OAAO,QAAQ;GACf,OAAO,eAAe;IACpB,MAAM,cAAc,OAAO;IAE3B,IAAI,aAAa;KACf,MAAM,WAAW,IAAI,YAAY;MAC/B;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;KACF,CAAC;KAED,UAAU,UAAU;KACpB,SAAS,KAAK;IAChB;GACF;GACA,SAAS,KAAK,YAAY,MAAM;EAClC;CACF,GAAG;EACD;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,CAAC;CAED,gBAAgB;EACd,WAAW;EAEX,aAAa;GACX,IAAI,UAAU,SAAS;IACrB,UAAU,QAAQ,QAAQ;IAC1B,UAAU,UAAU;GACtB;EACF;CACF,GAAG,CAAC,UAAU,CAAC;CAGf,OAAO;AACT;;;AC5GA,MAAa,iBAAiD;CAC5D,QAAQ;CACR,OAAO;CACP,wBAAwB;CACxB,YAAY;CACZ,kBAAkB;CAClB,UAAU;CACV,QAAQ;CACR,gBAAgB;CAChB,cAAc;CACd,OAAO;CACP,kBAAkB;AACpB;AAEA,MAAa,mBAAmB;;;ACzDhC,SAAgB,uBAA+B;CAC7C,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8LT;;;AC5LA,MAAM,WAAW;AAEjB,SAAgB,uBAA6B;CAC3C,IAAI,OAAO,aAAa,aACtB;CAEF,IAAI,SAAS,eAAe,QAAQ,GAClC;CAGF,MAAM,QAAQ,SAAS,cAAc,OAAO;CAC5C,MAAM,KAAK;CACX,MAAM,cAAc,eAAe;CACnC,SAAS,KAAK,YAAY,KAAK;AACjC;AAEA,SAAS,iBAAyB;CAChC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;aA2FI,eAAe,cAAc;;;;;;;;;;;;;;;;;;aAkB7B,eAAe,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4IxC,qBAAqB,EAAE;;;;;;;;;;;;;;;;;AAiBzB;;;AC7QA,MAAM,qBAAqB;AAE3B,SAAS,WAAW,QAA2B;CAC7C,IAAI,OAAO,OAAO,cAAc,YAAY;EAC1C,OAAO,UAAU;EACjB;CACF;CACA,OAAO,OAAO;AAChB;AAEA,SAAS,YAAY,QAA2B;CAC9C,IAAI,OAAO,OAAO,UAAU,YAAY;EACtC,OAAO,MAAM;EACb;CACF;CACA,OAAO,OAAO;AAChB;AAEA,SAAS,eACP,MACA,WACA,eACA;CACA,gBAAgB;EACd,MAAM,SAAS,UAAU;EACzB,IAAI,EAAE,QAAQ,WAAW,OAAO,MAC9B;EAEF,WAAW,MAAM;EACjB,cAAc,SAAS,MAAM;EAC7B,aAAa,YAAY,MAAM;CACjC,GAAG;EAAC;EAAM;EAAW;CAAa,CAAC;AACrC;AAEA,SAAS,mBACP,MACA,WACA,WACA;CACA,gBAAgB;EACd,MAAM,SAAS,UAAU;EACzB,IAAI,EAAE,QAAQ,SACZ;EAEF,MAAM,WAAW,UAAsB;GACrC,IAAI,MAAM,WAAW,QACnB,UAAU;EAEd;EACA,OAAO,iBAAiB,SAAS,OAAO;EACxC,aAAa,OAAO,oBAAoB,SAAS,OAAO;CAC1D,GAAG;EAAC;EAAM;EAAW;CAAS,CAAC;AACjC;AAEA,SAAS,cAAc,MAAe;CACpC,gBAAgB;EACd,IAAI,CAAC,MACH;EAEF,MAAM,OAAO,SAAS;EACtB,MAAM,mBAAmB,KAAK,MAAM;EACpC,MAAM,iBAAiB,KAAK,MAAM,iBAAiB,kBAAkB;EACrE,KAAK,MAAM,YAAY,oBAAoB,QAAQ;EACnD,KAAK,MAAM,WAAW;EACtB,aAAa;GACX,KAAK,MAAM,WAAW;GACtB,IAAI,gBACF,KAAK,MAAM,YAAY,oBAAoB,cAAc;QAEzD,KAAK,MAAM,eAAe,kBAAkB;EAEhD;CACF,GAAG,CAAC,IAAI,CAAC;AACX;AAEA,SAAS,gBACP,cACA,WACA;CACA,gBAAgB;EACd,qBAAqB;CACvB,GAAG,CAAC,CAAC;CAEL,gBAAgB;EACd,IAAI,EAAE,gBAAgB,UAAU,UAC9B;EAEF,MAAM,OAAO,UAAU,QAAQ,QAAQ,wBAAwB;EAC/D,IAAI,gBAAgB,aAClB,KAAK,MAAM,YAAY,oBAAoB,YAAY;CAE3D,GAAG,CAAC,cAAc,SAAS,CAAC;AAC9B;AAEA,SAAgB,kBAAkB,EAChC,MACA,cACA,WAAW,eACX,SACA,MAAM,UACN,WAAW,eACX,cACA,QAAQ,YACR,UACA,QACA,WACqC;CACrC,MAAM,UAAU,WAAW,aAAa;CACxC,MAAM,YAAY,iBAAiB,SAAS;CAC5C,MAAM,OAAO,YAAY,SAAS;CAClC,MAAM,YAAY,iBAAiB,SAAS;CAE5C,MAAM,SAAS;EAAE,GAAG;EAAgB,GAAG;CAAW;CAElD,MAAM,CAAC,OAAO,YAAY,SAAS,EAAE;CACrC,MAAM,CAAC,aAAa,kBAAkB,SAAS,EAAE;CACjD,MAAM,CAAC,OAAO,YAAY,SAAS,EAAE;CACrC,MAAM,CAAC,UAAU,eAAe,SAAS,EAAE;CAC3C,MAAM,CAAC,cAAc,mBAAmB,SAAS,KAAK;CACtD,MAAM,CAAC,WAAW,gBAAgB,SAAS,KAAK;CAChD,MAAM,CAAC,OAAO,YAAY,SAAwB,IAAI;CACtD,MAAM,CAAC,WAAW,gBAAgB,SAAS,KAAK;CAEhD,MAAM,gBAAgB,OAAyB,IAAI;CACnD,MAAM,YAAY,OAA0B,IAAI;CAEhD,MAAM,cAAc,EAAE,QAAQ;CAE9B,gBAAgB,cAAc,SAAS;CACvC,eAAe,MAAM,WAAW,aAAa;CAC7C,cAAc,IAAI;CAElB,gBAAgB;EACd,IAAI,MACF,SAAS;CAEb,GAAG,CAAC,MAAM,MAAM,CAAC;CAEjB,MAAM,kBAAkB;EACtB,SAAS,EAAE;EACX,eAAe,EAAE;EACjB,SAAS,EAAE;EACX,YAAY,EAAE;EACd,SAAS,IAAI;EACb,aAAa,KAAK;EAClB,gBAAgB,KAAK;CACvB;CAEA,MAAM,oBAAoB;EACxB,aAAa,IAAI;EACjB,iBAAiB;GACf,aAAa,KAAK;GAClB,aAAa,KAAK;GAClB,UAAU;GACV,UAAU;EACZ,GAAG,kBAAkB;CACvB;CAEA,mBAAmB,MAAM,WAAW,WAAW;CAE/C,MAAM,yBAAyB;EAG7B,OAAO,GAFM,YAAY,KAAK,KAAK,oCACnB,eAAe,QAAQ,qBAAqB,UAAU;CAExE;CAEA,MAAM,eAAe,OAAO,UAAqB;EAC/C,MAAM,eAAe;EAErB,IAAI,UAAU;GACZ,aAAa,IAAI;GACjB;EACF;EAEA,IAAI,CAAC,WAAW;GACd,SACE,gEACF;GACA;EACF;EAEA,IAAI,CAAC,MAAM,KAAK,GAAG;GACjB,SAAS,OAAO,QAAQ;GACxB;EACF;EAEA,gBAAgB,IAAI;EACpB,SAAS,IAAI;EAEb,IAAI;GAEF,MAAM,SAAS,MAAM,IADF,OAAO;IAAE;IAAS;IAAW;IAAM;GAAU,CACtC,CAAC,CAAC,OAAO;IACjC,aAAa,iBAAiB;IAC9B,OAAO,MAAM,KAAK;GACpB,CAAC;GAED,aAAa,IAAI;GACjB,WAAW,MAAM;GACjB,WAAW,aAAa,gBAAgB;EAC1C,SAAS,KAAK;GACZ,SACE,eAAe,QACX,IAAI,UACJ,yCACN;EACF,UAAU;GACR,gBAAgB,KAAK;EACvB;CACF;CAEA,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;AACF;;;AChPA,SAAgB,eAAe,EAC7B,MACA,cACA,WACA,SACA,MACA,WACA,QAAQ,QACR,cACA,QAAQ,YACR,UACA,QACA,WACsB;CACtB,MAAM,EACJ,QACA,OACA,UACA,aACA,gBACA,OACA,UACA,UACA,aACA,cACA,WACA,OACA,UACA,WACA,aACA,eACA,WACA,aACA,iBACE,kBAAkB;EACpB;EACA,QAAQ;EACR;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,CAAC;CAED,IAAI,CAAC,MACH,OAAO;CAGT,OACE,oBAAC,OAAD;EAAK,wBAAqB;EAAG,cAAY;EACvC,UAAA,oBAAC,UAAD;GACE,cAAY,OAAO;GACnB,WAAU;GACV,gBAAc,YAAY,SAAS,KAAA;GACnC,WAAW,UAAU;IACnB,MAAM,eAAe;IACrB,YAAY;GACd;GACA,KAAK;GAEJ,UAAA,YACC,qBAAC,OAAD;IAAK,WAAU;IAAf,UAAA;KACE,oBAAC,OAAD;MAAK,WAAU;MACb,UAAA,oBAAC,OAAD;OACE,eAAY;OACZ,MAAK;OACL,QAAO;OACP,eAAc;OACd,gBAAe;OACf,aAAY;OACZ,SAAQ;OAER,UAAA,oBAAC,QAAD,EAAM,GAAE,iBAAkB,CAAA;MACvB,CAAA;KACF,CAAA;KACL,oBAAC,MAAD;MAAI,WAAU;MAAwB,UAAA,OAAO;KAAiB,CAAA;KAC9D,oBAAC,KAAD;MAAG,WAAU;MAAuB,UAAA,OAAO;KAAkB,CAAA;IAC1D;GAEL,CAAA,IAAA,qBAAA,UAAA,EAAA,UAAA;IACE,qBAAC,OAAD;KAAK,WAAU;KAAf,UAAA,CACE,oBAAC,MAAD;MAAI,WAAU;MAAgB,UAAA,OAAO;KAAU,CAAA,GAC/C,oBAAC,UAAD;MACE,cAAY,OAAO;MACnB,WAAU;MACV,SAAS;MACT,MAAK;MAEL,UAAA,qBAAC,OAAD;OACE,eAAY;OACZ,MAAK;OACL,QAAO;OACP,eAAc;OACd,gBAAe;OACf,aAAY;OACZ,SAAQ;OAPV,UAAA,CASE,oBAAC,QAAD;QAAM,IAAG;QAAK,IAAG;QAAI,IAAG;QAAI,IAAG;OAAM,CAAA,GACrC,oBAAC,QAAD;QAAM,IAAG;QAAI,IAAG;QAAK,IAAG;QAAI,IAAG;OAAM,CAAA,CAClC;;KACC,CAAA,CACL;;IAEL,qBAAC,QAAD;KAAM,WAAU;KAAc,UAAU;KAAxC,UAAA;MACE,oBAAC,OAAD;OAAK,WAAU;OACb,UAAA,oBAAC,SAAD;QACE,cAAW;QACX,cAAa;QACb,WAAU;QACV,WAAW;QACX,WAAW,MAAM;SACf,SAAS,EAAE,OAAO,KAAK;SACvB,IAAI,OACF,SAAS,IAAI;QAEjB;QACA,aAAa,OAAO;QACpB,KAAK;QACL,UAAA;QACA,MAAK;QACL,OAAO;OACR,CAAA;MACE,CAAA;MAEL,oBAAC,OAAD;OAAK,WAAU;OACb,UAAA,oBAAC,YAAD;QACE,cAAW;QACX,WAAU;QACV,WAAW;QACX,WAAW,MAAM,eAAe,EAAE,OAAO,KAAK;QAC9C,aAAa,OAAO;QACpB,MAAM;QACN,OAAO;OACR,CAAA;MACE,CAAA;MAEJ,eACC,qBAAC,OAAD;OAAK,WAAU;OAAf,UAAA,CACE,oBAAC,SAAD;QAAO,WAAU;QAAe,SAAQ;QACrC,UAAA,OAAO;OACH,CAAA,GACP,oBAAC,SAAD;QACE,cAAa;QACb,WAAU;QACV,IAAG;QACH,WAAW,MAAM,SAAS,EAAE,OAAO,KAAK;QACxC,aAAa,OAAO;QACpB,MAAK;QACL,OAAO;OACR,CAAA,CACE;;MAGP,oBAAC,OAAD;OAAK,eAAY;OAAO,WAAU;OAChC,UAAA,oBAAC,SAAD;QACE,cAAa;QACb,MAAK;QACL,WAAW,MAAM,YAAY,EAAE,OAAO,KAAK;QAC3C,UAAU;QACV,MAAK;QACL,OAAO;OACR,CAAA;MACE,CAAA;MAEJ,SAAS,oBAAC,KAAD;OAAG,WAAU;OAAoB,UAAA;MAAS,CAAA;MAEpD,qBAAC,OAAD;OAAK,WAAU;OAAf,UAAA,CACE,oBAAC,UAAD;QACE,WAAU;QACV,SAAS;QACT,MAAK;QAEJ,UAAA,OAAO;OACF,CAAA,GACR,oBAAC,UAAD;QACE,WAAU;QACV,UAAU,gBAAgB,CAAC,MAAM,KAAK;QACtC,MAAK;QAEJ,UAAA,eACC,oBAAC,QAAD,EAAM,WAAU,iBAAkB,CAAA,IAElC,OAAO;OAEH,CAAA,CACL;;KACD;;IAEN,qBAAC,OAAD;KAAK,WAAU;KAAf,UAAA;MAAgC;MACnB;MACX,oBAAC,KAAD;OACE,MAAK;OACL,KAAI;OACJ,QAAO;OACR,UAAA;MAEE,CAAA;KACA;;GACL,EAAA,CAAA;EAEE,CAAA;CACL,CAAA;AAET;;;AChKA,SAAS,eAAe;CACtB,OACE,oBAAC,OAAD;EACE,eAAY;EACZ,MAAK;EACL,QAAO;EACP,QAAO;EACP,eAAc;EACd,gBAAe;EACf,aAAY;EACZ,SAAQ;EACR,OAAM;EAEN,UAAA,oBAAC,QAAD,EAAM,GAAE,gEAAiE,CAAA;CACtE,CAAA;AAET;AAMA,SAAgB,eAAe,EAC7B,WACA,SACA,MACA,WACA,QAAQ,QACR,cACA,QACA,UACA,QACA,SACA,WACA,UAAU,OACV,YACsB;CACtB,MAAM,CAAC,MAAM,WAAW,SAAS,KAAK;CAGtC,eAAe;EACb,qBAAqB;CACvB,CAAC;CAED,MAAM,cAAc,kBAAkB;EACpC,QAAQ,IAAI;CACd,GAAG,CAAC,CAAC;CAGL,IAAI;CAEJ,IAAI,WAAW,eAAe,QAAQ,GACpC,UAAU,aAAa,UAAoD,EACzE,SAAS,YACX,CAAC;MACI;EACL,MAAM,eAAe,QAAQ,WAAW;EACxC,UACE,oBAAC,OAAD;GAAK,wBAAqB;GAAG,cAAY;GACvC,UAAA,qBAAC,UAAD;IACE,cAAY;IACZ,WAAW,aAAa;IACxB,SAAS;IACT,OACE,eACK;KACC,oBAAoB;KACpB,0BAA0B;IAC5B,IACA,KAAA;IAEN,MAAK;IAZP,UAAA,CAcE,oBAAC,cAAD,CAAe,CAAA,GACd,YACK;;EACL,CAAA;CAET;CAEA,OACE,qBAAA,UAAA,EAAA,UAAA,CACG,SACD,oBAAC,gBAAD;EACW;EACD;EACC;EACD;EACR,cAAc;EACJ;EACJ;EACQ;EACH;EACJ;EACD;EACK;CACZ,CAAA,CACD,EAAA,CAAA;AAEN;;;ACjJA,SAAS,kBACP,UACsC;CACtC,MAAM,CAAC,WAAW,gBAAgB,SAAS,KAAK;CAChD,MAAM,CAAC,OAAO,YAAY,SAAuB,IAAI;CACrD,MAAM,CAAC,MAAM,WAAW,SAA4B;CA2BpD,OAAO;EAAE;EAAM;EAAO;EAAW,QAzBlB,YACb,OAAO,cAA0B;GAC/B,aAAa,IAAI;GACjB,SAAS,IAAI;GACb,IAAI;IACF,MAAM,SAAS,MAAM,SAAS,SAAS;IACvC,QAAQ,MAAM;IACd,OAAO;GACT,SAAS,KAAK;IACZ,MAAM,UACJ,eAAe,QAAQ,sBAAM,IAAI,MAAM,gBAAgB;IACzD,SAAS,OAAO;IAChB,MAAM;GACR,UAAU;IACR,aAAa,KAAK;GACpB;EACF,GACA,CAAC,QAAQ,CAQ2B;EAAG,OAL3B,kBAAkB;GAC9B,QAAQ,KAAA,CAAS;GACjB,SAAS,IAAI;EACf,GAAG,CAAC,CAEyC;CAAE;AACjD;;;;;;;;;;;;;;AAmBA,SAAgB,oBAGd;CACA,MAAM,SAAS,gBAAgB;CAK/B,OAAO,kBAJU,aACd,WAAiC,OAAO,OAAO,MAAM,GACtD,CAAC,MAAM,CAEuB,CAAC;AACnC;;;;;;;;;;;;;AAkBA,SAAgB,UAGd;CACA,MAAM,SAAS,gBAAgB;CAW/B,OAAO,kBAVU,aACd,EACC,YACA,OAAO,eAIH,OAAO,KAAK,YAAY,IAAI,GAClC,CAAC,MAAM,CAEuB,CAAC;AACnC;;;;AASA,SAAgB,gBAGd;CACA,MAAM,SAAS,gBAAgB;CAK/B,OAAO,kBAJU,aACd,WAA6B,OAAO,QAAQ,MAAM,GACnD,CAAC,MAAM,CAEuB,CAAC;AACnC;;;;AASA,SAAgB,kBAKd;CACA,MAAM,SAAS,gBAAgB;CAC/B,MAAM,CAAC,WAAW,gBAAgB,SAAS,KAAK;CAChD,MAAM,CAAC,OAAO,YAAY,SAAuB,IAAI;CAsCrD,OAAO;EAAE;EAAO;EAAW,WApCT,YAChB,OAAO,eAAuB;GAC5B,aAAa,IAAI;GACjB,SAAS,IAAI;GACb,IAAI;IACF,OAAO,MAAM,OAAO,UAAU,UAAU;GAC1C,SAAS,KAAK;IACZ,MAAM,UACJ,eAAe,QAAQ,sBAAM,IAAI,MAAM,qBAAqB;IAC9D,SAAS,OAAO;IAChB,MAAM;GACR,UAAU;IACR,aAAa,KAAK;GACpB;EACF,GACA,CAAC,MAAM,CAqB0B;EAAG,aAlBlB,YAClB,OAAO,eAAuB;GAC5B,aAAa,IAAI;GACjB,SAAS,IAAI;GACb,IAAI;IACF,OAAO,MAAM,OAAO,YAAY,UAAU;GAC5C,SAAS,KAAK;IACZ,MAAM,UACJ,eAAe,QAAQ,sBAAM,IAAI,MAAM,uBAAuB;IAChE,SAAS,OAAO;IAChB,MAAM;GACR,UAAU;IACR,aAAa,KAAK;GACpB;EACF,GACA,CAAC,MAAM,CAGuC;CAAE;AACpD;;;ACnKA,SAAS,eACP,SACA,UAAU,MACS;CACnB,MAAM,CAAC,MAAM,WAAW,SAAwB;CAChD,MAAM,CAAC,WAAW,gBAAgB,SAAS,OAAO;CAClD,MAAM,CAAC,OAAO,YAAY,SAAuB,IAAI;CAErD,MAAM,UAAU,YAAY,YAAY;EACtC,aAAa,IAAI;EACjB,SAAS,IAAI;EACb,IAAI;GACF,MAAM,SAAS,MAAM,QAAQ;GAC7B,QAAQ,MAAM;EAChB,SAAS,KAAK;GACZ,SAAS,eAAe,QAAQ,sBAAM,IAAI,MAAM,gBAAgB,CAAC;EACnE,UAAU;GACR,aAAa,KAAK;EACpB;CACF,GAAG,CAAC,OAAO,CAAC;CAEZ,gBAAgB;EACd,IAAI,SACF,QAAQ;CAEZ,GAAG,CAAC,SAAS,OAAO,CAAC;CAErB,OAAO;EAAE;EAAM;EAAO;EAAW;CAAQ;AAC3C;AAMA,SAAgB,wBAA4D;CAC1E,MAAM,SAAS,gBAAgB;CAE/B,OAAO,eADS,kBAAkB,OAAO,UAAU,GAAG,CAAC,MAAM,CACjC,CAAC;AAC/B;AAUA,SAAgB,gBACd,UAAkC,CAAC,GACyC;CAC5E,MAAM,SAAS,gBAAgB;CAC/B,MAAM,EACJ,UAAU,MACV,UACA,QACA,QACA,QACA,OACA,WACE;CAEJ,MAAM,SAAS,eACN;EAAE;EAAO;EAAQ;EAAQ;EAAQ;EAAQ;CAAS,IACzD;EAAC;EAAU;EAAQ;EAAQ;EAAQ;EAAO;CAAM,CAClD;CAGA,OAAO,eADS,kBAAkB,OAAO,KAAK,MAAM,GAAG,CAAC,QAAQ,MAAM,CAC1C,GAAG,OAAO;AACxC;AAMA,SAAgB,YACd,YACgC;CAChC,MAAM,SAAS,gBAAgB;CAS/B,OAAO,eAPS,kBAAkB;EAChC,IAAI,CAAC,YACH,OAAO,QAAQ,QAAQ,KAAA,CAAsC;EAE/D,OAAO,OAAO,IAAI,UAAU;CAC9B,GAAG,CAAC,QAAQ,UAAU,CAEM,GAAG,CAAC,CAAC,UAAU;AAC7C;AAMA,SAAgB,YACd,YACA,SAA8B,UACH;CAC3B,MAAM,SAAS,gBAAgB;CAS/B,OAAO,eAPS,kBAAkB;EAChC,IAAI,CAAC,YACH,OAAO,QAAQ,QAAQ,KAAA,CAAiC;EAE1D,OAAO,OAAO,YAAY,YAAY,MAAM;CAC9C,GAAG;EAAC;EAAQ;EAAY;CAAM,CAEF,GAAG,CAAC,CAAC,UAAU;AAC7C;AAMA,SAAgB,aAAsC;CACpD,MAAM,SAAS,gBAAgB;CAE/B,OAAO,eADS,kBAAkB,OAAO,WAAW,GAAG,CAAC,MAAM,CAClC,CAAC;AAC/B;AAMA,SAAgB,aAAa,OAAkD;CAC7E,MAAM,SAAS,gBAAgB;CAK/B,OAAO,eAJS,kBACR,OAAO,aAAa,KAAK,GAC/B,CAAC,QAAQ,KAAK,CAEY,CAAC;AAC/B;AAMA,MAAM,+BAA+B;AAErC,SAAgB,wBAAwB,WAGtC;CACA,MAAM,EAAE,MAAM,YAAY,aAAa;CACvC,MAAM,CAAC,aAAa,kBAAkB,SAAS,CAAC;CAEhD,MAAM,aAAa,GAAG,+BAA+B;CAErD,gBAAgB;EACd,IAAI,CAAC,WAAW,QAAQ,WAAW,GAAG;GACpC,eAAe,CAAC;GAChB;EACF;EAEA,IAAI,WAAW;EACf,IAAI;GACF,MAAM,SAAS,aAAa,QAAQ,UAAU;GAC9C,WAAW,SAAS,OAAO,MAAM,IAAI;EACvC,QAAQ,CAER;EAEA,IAAI,QAAQ;EACZ,KAAK,MAAM,SAAS,SAClB,IAAI,MAAM,eAAe,MAAM,cAAc,UAC3C;EAGJ,eAAe,KAAK;CACtB,GAAG,CAAC,SAAS,UAAU,CAAC;CAwBxB,OAAO;EAAE,YAtBU,kBAAkB;GACnC,IAAI,CAAC,WAAW,QAAQ,WAAW,GACjC;GAGF,IAAI,kBAAkB;GACtB,KAAK,MAAM,SAAS,SAClB,IAAI,MAAM,eAAe,MAAM,cAAc,iBAC3C,kBAAkB,MAAM;GAI5B,IAAI,kBAAkB,GAAG;IACvB,IAAI;KACF,aAAa,QAAQ,YAAY,OAAO,eAAe,CAAC;IAC1D,QAAQ,CAER;IACA,eAAe,CAAC;GAClB;EACF,GAAG,CAAC,SAAS,UAAU,CAEL;EAAG;CAAY;AACnC"}
|
|
1
|
+
{"version":3,"file":"react.js","names":[],"sources":["../src/react-changelog-widget.tsx","../src/react-feedback-dialog-types.ts","../src/react-feedback-styles-parts.ts","../src/react-feedback-styles.ts","../src/react-feedback-dialog-hook.ts","../src/react-feedback-dialog.tsx","../src/react-feedback-button.tsx","../src/react-mutation-hooks.ts","../src/react-query-hooks.ts"],"sourcesContent":["import { useCallback, useEffect, useRef } from \"react\";\nimport type { ChangelogEntry } from \"./types\";\n\ninterface ChangelogWidgetProps {\n autoOpenForNew?: boolean;\n locale?: string;\n maxEntries?: number;\n mode?: \"card\" | \"popup\" | \"trigger\";\n onClose?: () => void;\n onEntryClick?: (entry: ChangelogEntry) => void;\n onOpen?: () => void;\n position?: \"bottom-right\" | \"bottom-left\";\n primaryColor?: string;\n publicKey: string;\n theme?: \"light\" | \"dark\" | \"auto\";\n triggerSelector?: string;\n}\n\ninterface WidgetInstance {\n close: () => void;\n destroy: () => void;\n getUnreadCount: () => number;\n init: () => Promise<void>;\n markAsRead: () => void;\n open: () => void;\n}\n\ndeclare global {\n interface Window {\n RefletChangelogWidget?: new (\n config: ChangelogWidgetProps\n ) => WidgetInstance;\n }\n}\n\n/**\n * React component that wraps the Reflet Changelog Widget.\n *\n * Dynamically loads and initializes a RefletChangelogWidget instance.\n *\n * @example\n * ```tsx\n * import { ChangelogWidget } from 'reflet-sdk/react';\n *\n * export function MyApp() {\n * return <ChangelogWidget publicKey=\"fb_pub_xxx\" mode=\"card\" theme=\"auto\" />;\n * }\n * ```\n */\nexport function ChangelogWidget({\n publicKey,\n mode = \"card\",\n position = \"bottom-right\",\n theme = \"light\",\n primaryColor,\n maxEntries,\n triggerSelector,\n autoOpenForNew,\n locale,\n onOpen,\n onClose,\n onEntryClick,\n}: ChangelogWidgetProps) {\n const widgetRef = useRef<WidgetInstance | null>(null);\n\n const initWidget = useCallback(async () => {\n // Check if RefletChangelogWidget is available globally\n const WidgetClass = window.RefletChangelogWidget;\n\n if (WidgetClass) {\n const instance = new WidgetClass({\n autoOpenForNew,\n locale,\n maxEntries,\n mode,\n onClose,\n onEntryClick,\n onOpen,\n position,\n primaryColor,\n publicKey,\n theme,\n triggerSelector,\n });\n\n widgetRef.current = instance;\n await instance.init();\n return;\n }\n\n // Try loading the script dynamically\n const existingScript = document.querySelector(\n 'script[src*=\"reflet-changelog\"]'\n );\n if (!existingScript) {\n const script = document.createElement(\"script\");\n script.src = \"https://cdn.reflet.app/widget/reflet-changelog.v1.js\";\n script.async = true;\n script.onload = () => {\n const LoadedClass = window.RefletChangelogWidget;\n\n if (LoadedClass) {\n const instance = new LoadedClass({\n autoOpenForNew,\n locale,\n maxEntries,\n mode,\n onClose,\n onEntryClick,\n onOpen,\n position,\n primaryColor,\n publicKey,\n theme,\n triggerSelector,\n });\n\n widgetRef.current = instance;\n instance.init();\n }\n };\n document.head.appendChild(script);\n }\n }, [\n publicKey,\n mode,\n position,\n theme,\n primaryColor,\n maxEntries,\n triggerSelector,\n autoOpenForNew,\n locale,\n onOpen,\n onClose,\n onEntryClick,\n ]);\n\n useEffect(() => {\n initWidget();\n\n return () => {\n if (widgetRef.current) {\n widgetRef.current.destroy();\n widgetRef.current = null;\n }\n };\n }, [initWidget]);\n\n // This component doesn't render visible DOM - the widget manages its own Shadow DOM\n return null;\n}\n","import type { CreateFeedbackResponse, RefletUser } from \"./types\";\n\nexport interface FeedbackDialogLabels {\n cancel?: string;\n close?: string;\n descriptionPlaceholder?: string;\n emailLabel?: string;\n emailPlaceholder?: string;\n required?: string;\n submit?: string;\n successMessage?: string;\n successTitle?: string;\n title?: string;\n titlePlaceholder?: string;\n}\n\nexport interface FeedbackDialogProps {\n /** API base URL override */\n baseUrl?: string;\n /** Custom labels / i18n */\n labels?: FeedbackDialogLabels;\n /** Called when dialog closes */\n onClose?: () => void;\n /** Called when dialog opens */\n onOpen?: () => void;\n /** Callback when open state changes */\n onOpenChange: (open: boolean) => void;\n /** Called after successful submission */\n onSubmit?: (result: CreateFeedbackResponse) => void;\n /** Whether the dialog is open */\n open: boolean;\n /** Primary brand color (CSS value) */\n primaryColor?: string;\n /** Public API key (falls back to RefletProvider context) */\n publicKey?: string;\n /** Color theme */\n theme?: \"light\" | \"dark\" | \"auto\";\n /** User identification (falls back to RefletProvider context) */\n user?: RefletUser;\n /** Pre-signed user token */\n userToken?: string;\n}\n\nexport const DEFAULT_LABELS: Required<FeedbackDialogLabels> = {\n cancel: \"Cancel\",\n close: \"Close\",\n descriptionPlaceholder: \"Tell us more... (optional)\",\n emailLabel: \"Email\",\n emailPlaceholder: \"your@email.com (optional)\",\n required: \"Required\",\n submit: \"Send Feedback\",\n successMessage: \"Your feedback has been received. We appreciate your input.\",\n successTitle: \"Thank you!\",\n title: \"Send Feedback\",\n titlePlaceholder: \"What's on your mind?\",\n};\n\nexport const AUTO_CLOSE_DELAY = 2500;\n","export function getFeedbackChromeCSS(): string {\n return `\n.reflet-footer {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n padding-top: 4px;\n}\n\n.reflet-btn {\n min-height: 40px;\n padding: 10px 20px;\n border: none;\n border-radius: var(--reflet-radius-sm);\n font-family: var(--reflet-font);\n font-size: 14px;\n font-weight: 500;\n cursor: pointer;\n transition: background 150ms, color 150ms, opacity 150ms, transform 150ms var(--reflet-ease);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n line-height: 1;\n}\n\n.reflet-btn-primary {\n background: var(--reflet-primary);\n color: var(--reflet-on-primary);\n}\n\n.reflet-btn-primary:hover:not(:disabled) {\n background: var(--reflet-primary-hover);\n}\n\n.reflet-btn-primary:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.reflet-btn-secondary {\n background: transparent;\n color: var(--reflet-text-secondary);\n}\n\n.reflet-btn-secondary:hover {\n background: var(--reflet-bg-secondary);\n color: var(--reflet-text);\n}\n\n.reflet-spinner {\n width: 16px;\n height: 16px;\n border: 2px solid var(--reflet-on-primary-soft);\n border-top-color: var(--reflet-on-primary);\n border-radius: 50%;\n animation: reflet-spin 600ms linear infinite;\n}\n\n.reflet-success {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: 48px 24px;\n text-align: center;\n gap: 16px;\n}\n\n.reflet-success-icon {\n width: 56px;\n height: 56px;\n border-radius: 50%;\n background: color-mix(in srgb, var(--reflet-success) 12%, transparent);\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.reflet-success-icon svg {\n width: 28px;\n height: 28px;\n color: var(--reflet-success);\n}\n\n.reflet-success-icon svg path {\n stroke-dasharray: 24;\n stroke-dashoffset: 24;\n animation: reflet-checkmark 400ms ease-out 100ms forwards;\n}\n\n.reflet-success-title {\n font-size: 18px;\n font-weight: 600;\n color: var(--reflet-text);\n margin: 0;\n}\n\n.reflet-success-text {\n font-size: 14px;\n color: var(--reflet-text-secondary);\n margin: 0;\n}\n\n.reflet-error-msg {\n font-size: 13px;\n color: var(--reflet-error);\n margin: 0;\n}\n\n.reflet-trigger {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n min-height: 40px;\n padding: 10px 18px;\n background: var(--reflet-primary);\n color: var(--reflet-on-primary);\n border: none;\n border-radius: var(--reflet-radius-sm);\n font-family: var(--reflet-font);\n font-size: 14px;\n font-weight: 500;\n cursor: pointer;\n transition: background 150ms, transform 150ms var(--reflet-ease);\n line-height: 1;\n}\n\n.reflet-trigger:hover {\n background: var(--reflet-primary-hover);\n}\n\n.reflet-trigger svg {\n width: 16px;\n height: 16px;\n}\n\n.reflet-hp {\n position: absolute;\n top: -9999px;\n left: -9999px;\n opacity: 0;\n height: 0;\n width: 0;\n z-index: -1;\n}\n\n.reflet-powered {\n text-align: center;\n padding: 0 24px 16px;\n font-size: 11px;\n color: var(--reflet-text-tertiary);\n}\n\n.reflet-powered a {\n color: var(--reflet-text-secondary);\n text-decoration: none;\n font-weight: 500;\n}\n\n.reflet-powered a:hover {\n color: var(--reflet-primary);\n}\n\n@media (max-width: 640px) {\n .reflet-header::before {\n content: '';\n position: absolute;\n top: 8px;\n left: 50%;\n transform: translateX(-50%);\n width: 36px;\n height: 4px;\n border-radius: 2px;\n background: var(--reflet-border);\n }\n\n .reflet-header {\n position: relative;\n padding-top: 24px;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .reflet-success-icon svg path {\n stroke-dasharray: none;\n stroke-dashoffset: 0;\n animation: none;\n }\n}\n`;\n}\n","import { getFeedbackChromeCSS } from \"./react-feedback-styles-parts\";\nimport { REFLET_Z_INDEX } from \"./z-index\";\n\nconst STYLE_ID = \"reflet-feedback-styles\";\n\nexport function injectFeedbackStyles(): void {\n if (typeof document === \"undefined\") {\n return;\n }\n if (document.getElementById(STYLE_ID)) {\n return;\n }\n\n const style = document.createElement(\"style\");\n style.id = STYLE_ID;\n style.textContent = getFeedbackCSS();\n document.head.appendChild(style);\n}\n\nfunction getFeedbackCSS(): string {\n return `\n:root {\n --reflet-scrim: rgb(0 0 0 / 45%);\n}\n\n[data-reflet-feedback] {\n --reflet-primary: #6366f1;\n --reflet-primary-hover: #4f46e5;\n --reflet-on-primary: #ffffff;\n --reflet-on-primary-soft: rgb(255 255 255 / 30%);\n --reflet-bg: #ffffff;\n --reflet-bg-secondary: #f9fafb;\n --reflet-text: #111827;\n --reflet-text-secondary: #6b7280;\n --reflet-text-tertiary: #9ca3af;\n --reflet-border: #e5e7eb;\n --reflet-radius: 12px;\n --reflet-radius-sm: 8px;\n --reflet-shadow: 0 20px 60px -12px rgb(0 0 0 / 25%), 0 0 0 1px rgb(0 0 0 / 5%);\n --reflet-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n --reflet-success: #22c55e;\n --reflet-error: #ef4444;\n --reflet-ease: cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n@media (prefers-color-scheme: dark) {\n [data-reflet-feedback][data-theme=\"auto\"] {\n --reflet-bg: #1f2937;\n --reflet-bg-secondary: #111827;\n --reflet-text: #f9fafb;\n --reflet-text-secondary: #9ca3af;\n --reflet-text-tertiary: #6b7280;\n --reflet-border: #374151;\n --reflet-shadow: 0 20px 60px -12px rgb(0 0 0 / 50%), 0 0 0 1px rgb(255 255 255 / 5%);\n }\n}\n\n[data-reflet-feedback][data-theme=\"dark\"] {\n --reflet-bg: #1f2937;\n --reflet-bg-secondary: #111827;\n --reflet-text: #f9fafb;\n --reflet-text-secondary: #9ca3af;\n --reflet-text-tertiary: #6b7280;\n --reflet-border: #374151;\n --reflet-shadow: 0 20px 60px -12px rgb(0 0 0 / 50%), 0 0 0 1px rgb(255 255 255 / 5%);\n}\n\n@keyframes reflet-fade-in {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n@keyframes reflet-fade-out {\n from { opacity: 1; }\n to { opacity: 0; }\n}\n\n@keyframes reflet-slide-up {\n from { transform: translateY(100%); }\n to { transform: translateY(0); }\n}\n\n@keyframes reflet-slide-down {\n from { transform: translateY(0); }\n to { transform: translateY(100%); }\n}\n\n@keyframes reflet-scale-in {\n from { opacity: 0; transform: translate(-50%, -50%) scale(0.95); }\n to { opacity: 1; transform: translate(-50%, -50%) scale(1); }\n}\n\n@keyframes reflet-scale-out {\n from { opacity: 1; transform: translate(-50%, -50%) scale(1); }\n to { opacity: 0; transform: translate(-50%, -50%) scale(0.95); }\n}\n\n@keyframes reflet-checkmark {\n 0% { stroke-dashoffset: 24; }\n 100% { stroke-dashoffset: 0; }\n}\n\n@keyframes reflet-spin {\n to { transform: rotate(360deg); }\n}\n\n.reflet-dialog {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n z-index: ${REFLET_Z_INDEX.dialogContent};\n margin: 0;\n border: 0;\n padding: 0;\n background: var(--reflet-bg);\n border-radius: var(--reflet-radius);\n box-shadow: var(--reflet-shadow);\n width: 480px;\n max-width: calc(100vw - 32px);\n max-height: calc(100dvh - 64px);\n overflow: hidden;\n overscroll-behavior: contain;\n font-family: var(--reflet-font);\n color: var(--reflet-text);\n animation: reflet-scale-in 200ms var(--reflet-ease);\n}\n\n.reflet-dialog::backdrop {\n z-index: ${REFLET_Z_INDEX.dialogOverlay};\n background: var(--reflet-scrim);\n animation: reflet-fade-in 150ms ease-out;\n}\n\n.reflet-dialog[data-closing=\"true\"] {\n animation: reflet-scale-out 150ms ease-out forwards;\n}\n\n.reflet-dialog[data-closing=\"true\"]::backdrop {\n animation: reflet-fade-out 150ms ease-out forwards;\n}\n\n@media (max-width: 640px) {\n .reflet-dialog {\n top: auto;\n left: 0;\n right: 0;\n bottom: 0;\n transform: none;\n width: 100%;\n max-width: 100%;\n border-radius: var(--reflet-radius) var(--reflet-radius) 0 0;\n max-height: 90dvh;\n padding-bottom: env(safe-area-inset-bottom);\n animation: reflet-slide-up 300ms var(--reflet-ease);\n }\n\n .reflet-dialog[data-closing=\"true\"] {\n animation: reflet-slide-down 200ms ease-out forwards;\n }\n}\n\n.reflet-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 20px 24px 0;\n}\n\n.reflet-title {\n font-size: 18px;\n font-weight: 600;\n margin: 0;\n color: var(--reflet-text);\n line-height: 1.3;\n}\n\n.reflet-close {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 40px;\n height: 40px;\n border: none;\n background: transparent;\n border-radius: 8px;\n color: var(--reflet-text-secondary);\n cursor: pointer;\n padding: 0;\n flex-shrink: 0;\n transition: background 150ms, color 150ms, transform 150ms var(--reflet-ease);\n}\n\n.reflet-close:hover {\n background: var(--reflet-bg-secondary);\n color: var(--reflet-text);\n}\n\n.reflet-close svg {\n width: 18px;\n height: 18px;\n}\n\n.reflet-form {\n padding: 20px 24px 24px;\n display: flex;\n flex-direction: column;\n gap: 16px;\n}\n\n.reflet-field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.reflet-label {\n font-size: 13px;\n font-weight: 500;\n color: var(--reflet-text-secondary);\n}\n\n.reflet-input,\n.reflet-textarea {\n width: 100%;\n padding: 10px 12px;\n border: 1px solid var(--reflet-border);\n border-radius: var(--reflet-radius-sm);\n background: var(--reflet-bg);\n color: var(--reflet-text);\n font-family: var(--reflet-font);\n font-size: 14px;\n line-height: 1.5;\n outline: none;\n transition: border-color 150ms, box-shadow 150ms;\n box-sizing: border-box;\n}\n\n.reflet-input::placeholder,\n.reflet-textarea::placeholder {\n color: var(--reflet-text-tertiary);\n}\n\n.reflet-input:focus,\n.reflet-textarea:focus {\n border-color: var(--reflet-primary);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--reflet-primary) 15%, transparent);\n}\n\n.reflet-textarea {\n resize: vertical;\n min-height: 100px;\n}\n\n[data-reflet-feedback] :focus-visible {\n outline: 2px solid var(--reflet-primary);\n outline-offset: 2px;\n}\n\n[data-reflet-feedback] button:active:not(:disabled) {\n transform: scale(0.97);\n}\n\n@media (pointer: coarse), (max-width: 640px) {\n .reflet-input,\n .reflet-textarea {\n font-size: 16px;\n }\n}\n${getFeedbackChromeCSS()}\n@media (prefers-reduced-motion: reduce) {\n [data-reflet-feedback] *,\n [data-reflet-feedback] *::before,\n [data-reflet-feedback] *::after,\n .reflet-dialog,\n .reflet-dialog::backdrop {\n animation-duration: 1ms !important;\n animation-iteration-count: 1 !important;\n transition-duration: 1ms !important;\n }\n\n [data-reflet-feedback] button:active:not(:disabled) {\n transform: none;\n }\n}\n`;\n}\n","import {\n type FormEvent,\n type RefObject,\n useContext,\n useEffect,\n useRef,\n useState,\n} from \"react\";\nimport { Reflet } from \"./client\";\nimport { RefletContext } from \"./react-context\";\nimport {\n AUTO_CLOSE_DELAY,\n DEFAULT_LABELS,\n type FeedbackDialogProps,\n} from \"./react-feedback-dialog-types\";\nimport { injectFeedbackStyles } from \"./react-feedback-styles\";\n\nconst CLOSE_ANIMATION_MS = 200;\n\nfunction openDialog(dialog: HTMLDialogElement) {\n if (typeof dialog.showModal === \"function\") {\n dialog.showModal();\n return;\n }\n dialog.open = true;\n}\n\nfunction closeDialog(dialog: HTMLDialogElement) {\n if (typeof dialog.close === \"function\") {\n dialog.close();\n return;\n }\n dialog.open = false;\n}\n\nfunction useModalDialog(\n open: boolean,\n dialogRef: RefObject<HTMLDialogElement | null>,\n titleInputRef: RefObject<HTMLInputElement | null>\n) {\n useEffect(() => {\n const dialog = dialogRef.current;\n if (!(open && dialog) || dialog.open) {\n return;\n }\n openDialog(dialog);\n titleInputRef.current?.focus();\n return () => closeDialog(dialog);\n }, [open, dialogRef, titleInputRef]);\n}\n\nfunction useBackdropDismiss(\n open: boolean,\n dialogRef: RefObject<HTMLDialogElement | null>,\n onDismiss: () => void\n) {\n useEffect(() => {\n const dialog = dialogRef.current;\n if (!(open && dialog)) {\n return;\n }\n const dismiss = (event: MouseEvent) => {\n if (event.target === dialog) {\n onDismiss();\n }\n };\n dialog.addEventListener(\"click\", dismiss);\n return () => dialog.removeEventListener(\"click\", dismiss);\n }, [open, dialogRef, onDismiss]);\n}\n\nfunction useScrollLock(open: boolean) {\n useEffect(() => {\n if (!open) {\n return;\n }\n const root = document.documentElement;\n const previousOverflow = root.style.overflow;\n const previousGutter = root.style.getPropertyValue(\"scrollbar-gutter\");\n root.style.setProperty(\"scrollbar-gutter\", \"stable\");\n root.style.overflow = \"hidden\";\n return () => {\n root.style.overflow = previousOverflow;\n if (previousGutter) {\n root.style.setProperty(\"scrollbar-gutter\", previousGutter);\n } else {\n root.style.removeProperty(\"scrollbar-gutter\");\n }\n };\n }, [open]);\n}\n\nfunction usePrimaryColor(\n primaryColor: string | undefined,\n dialogRef: RefObject<HTMLDialogElement | null>\n) {\n useEffect(() => {\n injectFeedbackStyles();\n }, []);\n\n useEffect(() => {\n if (!(primaryColor && dialogRef.current)) {\n return;\n }\n const host = dialogRef.current.closest(\"[data-reflet-feedback]\");\n if (host instanceof HTMLElement) {\n host.style.setProperty(\"--reflet-primary\", primaryColor);\n }\n }, [primaryColor, dialogRef]);\n}\n\nexport function useFeedbackDialog({\n open,\n onOpenChange,\n publicKey: publicKeyProp,\n baseUrl,\n user: userProp,\n userToken: userTokenProp,\n primaryColor,\n labels: labelsProp,\n onSubmit,\n onOpen,\n onClose,\n}: Omit<FeedbackDialogProps, \"theme\">) {\n const context = useContext(RefletContext);\n const publicKey = publicKeyProp ?? context?.publicKey;\n const user = userProp ?? context?.user;\n const userToken = userTokenProp ?? context?.userToken;\n\n const labels = { ...DEFAULT_LABELS, ...labelsProp };\n\n const [title, setTitle] = useState(\"\");\n const [description, setDescription] = useState(\"\");\n const [email, setEmail] = useState(\"\");\n const [honeypot, setHoneypot] = useState(\"\");\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [isSuccess, setIsSuccess] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [isClosing, setIsClosing] = useState(false);\n\n const titleInputRef = useRef<HTMLInputElement>(null);\n const dialogRef = useRef<HTMLDialogElement>(null);\n\n const isAnonymous = !(user || userToken);\n\n usePrimaryColor(primaryColor, dialogRef);\n useModalDialog(open, dialogRef, titleInputRef);\n useScrollLock(open);\n\n useEffect(() => {\n if (open) {\n onOpen?.();\n }\n }, [open, onOpen]);\n\n const resetForm = () => {\n setTitle(\"\");\n setDescription(\"\");\n setEmail(\"\");\n setHoneypot(\"\");\n setError(null);\n setIsSuccess(false);\n setIsSubmitting(false);\n };\n\n const handleClose = () => {\n setIsClosing(true);\n setTimeout(() => {\n setIsClosing(false);\n onOpenChange(false);\n onClose?.();\n resetForm();\n }, CLOSE_ANIMATION_MS);\n };\n\n useBackdropDismiss(open, dialogRef, handleClose);\n\n const buildDescription = () => {\n const body = description.trim() || \"No additional details provided.\";\n const contact = isAnonymous && email ? `\\n\\n---\\nContact: ${email}` : \"\";\n return `${body}${contact}`;\n };\n\n const handleSubmit = async (event: FormEvent) => {\n event.preventDefault();\n\n if (honeypot) {\n setIsSuccess(true);\n return;\n }\n\n if (!publicKey) {\n setError(\n \"Missing publicKey. Provide it as a prop or via RefletProvider.\"\n );\n return;\n }\n\n if (!title.trim()) {\n setError(labels.required);\n return;\n }\n\n setIsSubmitting(true);\n setError(null);\n\n try {\n const client = new Reflet({ baseUrl, publicKey, user, userToken });\n const result = await client.create({\n description: buildDescription(),\n title: title.trim(),\n });\n\n setIsSuccess(true);\n onSubmit?.(result);\n setTimeout(handleClose, AUTO_CLOSE_DELAY);\n } catch (err) {\n setError(\n err instanceof Error\n ? err.message\n : \"Something went wrong. Please try again.\"\n );\n } finally {\n setIsSubmitting(false);\n }\n };\n\n return {\n description,\n dialogRef,\n email,\n error,\n handleClose,\n handleSubmit,\n honeypot,\n isAnonymous,\n isClosing,\n isSubmitting,\n isSuccess,\n labels,\n setDescription,\n setEmail,\n setError,\n setHoneypot,\n setTitle,\n title,\n titleInputRef,\n };\n}\n","import { useFeedbackDialog } from \"./react-feedback-dialog-hook\";\nimport type { FeedbackDialogProps } from \"./react-feedback-dialog-types\";\n\nexport type {\n FeedbackDialogLabels,\n FeedbackDialogProps,\n} from \"./react-feedback-dialog-types\";\n\nexport function FeedbackDialog({\n open,\n onOpenChange,\n publicKey,\n baseUrl,\n user,\n userToken,\n theme = \"auto\",\n primaryColor,\n labels: labelsProp,\n onSubmit,\n onOpen,\n onClose,\n}: FeedbackDialogProps) {\n const {\n labels,\n title,\n setTitle,\n description,\n setDescription,\n email,\n setEmail,\n honeypot,\n setHoneypot,\n isSubmitting,\n isSuccess,\n error,\n setError,\n isClosing,\n isAnonymous,\n titleInputRef,\n dialogRef,\n handleClose,\n handleSubmit,\n } = useFeedbackDialog({\n baseUrl,\n labels: labelsProp,\n onClose,\n onOpen,\n onOpenChange,\n onSubmit,\n open,\n primaryColor,\n publicKey,\n user,\n userToken,\n });\n\n if (!open) {\n return null;\n }\n\n return (\n <div data-reflet-feedback=\"\" data-theme={theme}>\n <dialog\n aria-label={labels.title}\n className=\"reflet-dialog\"\n data-closing={isClosing ? \"true\" : undefined}\n onCancel={(event) => {\n event.preventDefault();\n handleClose();\n }}\n ref={dialogRef}\n >\n {isSuccess ? (\n <div className=\"reflet-success\">\n <div className=\"reflet-success-icon\">\n <svg\n aria-hidden=\"true\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth=\"2.5\"\n viewBox=\"0 0 24 24\"\n >\n <path d=\"M5 13l4 4L19 7\" />\n </svg>\n </div>\n <h2 className=\"reflet-success-title\">{labels.successTitle}</h2>\n <p className=\"reflet-success-text\">{labels.successMessage}</p>\n </div>\n ) : (\n <>\n <div className=\"reflet-header\">\n <h2 className=\"reflet-title\">{labels.title}</h2>\n <button\n aria-label={labels.close}\n className=\"reflet-close\"\n onClick={handleClose}\n type=\"button\"\n >\n <svg\n aria-hidden=\"true\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth=\"2\"\n viewBox=\"0 0 24 24\"\n >\n <line x1=\"18\" x2=\"6\" y1=\"6\" y2=\"18\" />\n <line x1=\"6\" x2=\"18\" y1=\"6\" y2=\"18\" />\n </svg>\n </button>\n </div>\n\n <form className=\"reflet-form\" onSubmit={handleSubmit}>\n <div className=\"reflet-field\">\n <input\n aria-label=\"Feedback title\"\n autoComplete=\"off\"\n className=\"reflet-input\"\n maxLength={100}\n onChange={(e) => {\n setTitle(e.target.value);\n if (error) {\n setError(null);\n }\n }}\n placeholder={labels.titlePlaceholder}\n ref={titleInputRef}\n required\n type=\"text\"\n value={title}\n />\n </div>\n\n <div className=\"reflet-field\">\n <textarea\n aria-label=\"Feedback description\"\n className=\"reflet-textarea\"\n maxLength={2000}\n onChange={(e) => setDescription(e.target.value)}\n placeholder={labels.descriptionPlaceholder}\n rows={3}\n value={description}\n />\n </div>\n\n {isAnonymous && (\n <div className=\"reflet-field\">\n <label className=\"reflet-label\" htmlFor=\"reflet-email\">\n {labels.emailLabel}\n </label>\n <input\n autoComplete=\"email\"\n className=\"reflet-input\"\n id=\"reflet-email\"\n onChange={(e) => setEmail(e.target.value)}\n placeholder={labels.emailPlaceholder}\n type=\"email\"\n value={email}\n />\n </div>\n )}\n\n <div aria-hidden=\"true\" className=\"reflet-hp\">\n <input\n autoComplete=\"off\"\n name=\"website\"\n onChange={(e) => setHoneypot(e.target.value)}\n tabIndex={-1}\n type=\"text\"\n value={honeypot}\n />\n </div>\n\n {error && <p className=\"reflet-error-msg\">{error}</p>}\n\n <div className=\"reflet-footer\">\n <button\n className=\"reflet-btn reflet-btn-secondary\"\n onClick={handleClose}\n type=\"button\"\n >\n {labels.cancel}\n </button>\n <button\n className=\"reflet-btn reflet-btn-primary\"\n disabled={isSubmitting || !title.trim()}\n type=\"submit\"\n >\n {isSubmitting ? (\n <span className=\"reflet-spinner\" />\n ) : (\n labels.submit\n )}\n </button>\n </div>\n </form>\n\n <div className=\"reflet-powered\">\n Powered by{\" \"}\n <a\n href=\"https://reflet.app\"\n rel=\"noopener noreferrer\"\n target=\"_blank\"\n >\n Reflet\n </a>\n </div>\n </>\n )}\n </dialog>\n </div>\n );\n}\n","import {\n cloneElement,\n isValidElement,\n type ReactElement,\n type ReactNode,\n useCallback,\n useState,\n} from \"react\";\nimport {\n FeedbackDialog,\n type FeedbackDialogLabels,\n} from \"./react-feedback-dialog\";\nimport { injectFeedbackStyles } from \"./react-feedback-styles\";\nimport type { CreateFeedbackResponse, RefletUser } from \"./types\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface FeedbackButtonProps {\n /**\n * Render as child element instead of the default button.\n * When true, the first child element receives the onClick handler.\n */\n asChild?: boolean;\n /** API base URL override */\n baseUrl?: string;\n /** Custom trigger content or element (used with asChild) */\n children?: ReactNode;\n /** Custom CSS class for the default trigger button */\n className?: string;\n /** Custom labels / i18n */\n labels?: FeedbackDialogLabels & { trigger?: string };\n /** Called when dialog closes */\n onClose?: () => void;\n /** Called when dialog opens */\n onOpen?: () => void;\n /** Called after successful submission */\n onSubmit?: (result: CreateFeedbackResponse) => void;\n /** Primary brand color (CSS value) */\n primaryColor?: string;\n /** Public API key (falls back to RefletProvider context) */\n publicKey?: string;\n /** Color theme */\n theme?: \"light\" | \"dark\" | \"auto\";\n /** User identification (falls back to RefletProvider context) */\n user?: RefletUser;\n /** Pre-signed user token */\n userToken?: string;\n}\n\n// ============================================\n// Feedback Icon SVG\n// ============================================\n\nfunction FeedbackIcon() {\n return (\n <svg\n aria-hidden=\"true\"\n fill=\"none\"\n height=\"16\"\n stroke=\"currentColor\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth=\"2\"\n viewBox=\"0 0 24 24\"\n width=\"16\"\n >\n <path d=\"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\" />\n </svg>\n );\n}\n\n// ============================================\n// Component\n// ============================================\n\nexport function FeedbackButton({\n publicKey,\n baseUrl,\n user,\n userToken,\n theme = \"auto\",\n primaryColor,\n labels,\n onSubmit,\n onOpen,\n onClose,\n className,\n asChild = false,\n children,\n}: FeedbackButtonProps) {\n const [open, setOpen] = useState(false);\n\n // Inject styles so the trigger button is styled\n useState(() => {\n injectFeedbackStyles();\n });\n\n const handleClick = useCallback(() => {\n setOpen(true);\n }, []);\n\n // Render trigger\n let trigger: ReactNode;\n\n if (asChild && isValidElement(children)) {\n trigger = cloneElement(children as ReactElement<{ onClick?: () => void }>, {\n onClick: handleClick,\n });\n } else {\n const triggerLabel = labels?.trigger ?? \"Feedback\";\n trigger = (\n <div data-reflet-feedback=\"\" data-theme={theme}>\n <button\n aria-label={triggerLabel}\n className={className ?? \"reflet-trigger\"}\n onClick={handleClick}\n style={\n primaryColor\n ? ({\n \"--reflet-primary\": primaryColor,\n \"--reflet-primary-hover\": primaryColor,\n } as React.CSSProperties)\n : undefined\n }\n type=\"button\"\n >\n <FeedbackIcon />\n {triggerLabel}\n </button>\n </div>\n );\n }\n\n return (\n <>\n {trigger}\n <FeedbackDialog\n baseUrl={baseUrl}\n labels={labels}\n onClose={onClose}\n onOpen={onOpen}\n onOpenChange={setOpen}\n onSubmit={onSubmit}\n open={open}\n primaryColor={primaryColor}\n publicKey={publicKey}\n theme={theme}\n user={user}\n userToken={userToken}\n />\n </>\n );\n}\n","import { useCallback, useState } from \"react\";\nimport { useRefletClient } from \"./react-context\";\nimport type { UseMutationResult } from \"./react-hooks-types\";\nimport type { AddCommentParams, CreateFeedbackParams } from \"./types\";\n\n// ============================================\n// Generic mutation hook — single source of truth\n// ============================================\n\nfunction useRefletMutation<TData, TVariables>(\n mutateFn: (variables: TVariables) => Promise<TData>\n): UseMutationResult<TData, TVariables> {\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n const [data, setData] = useState<TData | undefined>();\n\n const mutate = useCallback(\n async (variables: TVariables) => {\n setIsLoading(true);\n setError(null);\n try {\n const result = await mutateFn(variables);\n setData(result);\n return result;\n } catch (err) {\n const wrapped =\n err instanceof Error ? err : new Error(\"Request failed\");\n setError(wrapped);\n throw wrapped;\n } finally {\n setIsLoading(false);\n }\n },\n [mutateFn]\n );\n\n const reset = useCallback(() => {\n setData(undefined);\n setError(null);\n }, []);\n\n return { data, error, isLoading, mutate, reset };\n}\n\n// ============================================\n// Create Feedback Mutation Hook\n// ============================================\n\n/**\n * Hook to create new feedback\n *\n * @example\n * ```tsx\n * const { mutate: createFeedback, isLoading } = useCreateFeedback();\n *\n * const handleSubmit = async (data) => {\n * const result = await createFeedback(data);\n * console.log('Created:', result.feedbackId);\n * };\n * ```\n */\nexport function useCreateFeedback(): UseMutationResult<\n { feedbackId: string; isApproved: boolean },\n CreateFeedbackParams\n> {\n const client = useRefletClient();\n const mutateFn = useCallback(\n (params: CreateFeedbackParams) => client.create(params),\n [client]\n );\n return useRefletMutation(mutateFn);\n}\n\n// ============================================\n// Vote Mutation Hook\n// ============================================\n\n/**\n * Hook to vote on feedback\n *\n * @example\n * ```tsx\n * const { mutate: vote } = useVote();\n *\n * <button onClick={() => vote({ feedbackId: 'xxx' })}>\n * Vote\n * </button>\n * ```\n */\nexport function useVote(): UseMutationResult<\n { voted: boolean; voteCount: number },\n { feedbackId: string; type?: \"upvote\" | \"downvote\" }\n> {\n const client = useRefletClient();\n const mutateFn = useCallback(\n ({\n feedbackId,\n type = \"upvote\",\n }: {\n feedbackId: string;\n type?: \"upvote\" | \"downvote\";\n }) => client.vote(feedbackId, type),\n [client]\n );\n return useRefletMutation(mutateFn);\n}\n\n// ============================================\n// Comment Mutation Hook\n// ============================================\n\n/**\n * Hook to add a comment\n */\nexport function useAddComment(): UseMutationResult<\n { commentId: string },\n AddCommentParams\n> {\n const client = useRefletClient();\n const mutateFn = useCallback(\n (params: AddCommentParams) => client.comment(params),\n [client]\n );\n return useRefletMutation(mutateFn);\n}\n\n// ============================================\n// Subscribe Mutation Hook\n// ============================================\n\n/**\n * Hook to subscribe/unsubscribe to feedback\n */\nexport function useSubscription(): {\n subscribe: (feedbackId: string) => Promise<{ subscribed: boolean }>;\n unsubscribe: (feedbackId: string) => Promise<{ unsubscribed: boolean }>;\n isLoading: boolean;\n error: Error | null;\n} {\n const client = useRefletClient();\n const subscribeFn = useCallback(\n (feedbackId: string) => client.subscribe(feedbackId),\n [client]\n );\n const unsubscribeFn = useCallback(\n (feedbackId: string) => client.unsubscribe(feedbackId),\n [client]\n );\n const subscription = useRefletMutation(subscribeFn);\n const unsubscription = useRefletMutation(unsubscribeFn);\n const { mutate: runSubscribe, reset: resetSubscribe } = subscription;\n const { mutate: runUnsubscribe, reset: resetUnsubscribe } = unsubscription;\n\n const subscribe = useCallback(\n (feedbackId: string) => {\n resetUnsubscribe();\n return runSubscribe(feedbackId);\n },\n [resetUnsubscribe, runSubscribe]\n );\n\n const unsubscribe = useCallback(\n (feedbackId: string) => {\n resetSubscribe();\n return runUnsubscribe(feedbackId);\n },\n [resetSubscribe, runUnsubscribe]\n );\n\n return {\n error: subscription.error ?? unsubscription.error,\n isLoading: subscription.isLoading || unsubscription.isLoading,\n subscribe,\n unsubscribe,\n };\n}\n","import {\n useCallback,\n useEffect,\n useMemo,\n useState,\n useSyncExternalStore,\n} from \"react\";\nimport { useRefletClient } from \"./react-context\";\nimport type { UseQueryResult } from \"./react-hooks-types\";\nimport type {\n ChangelogEntry,\n Comment,\n FeedbackDetail,\n FeedbackItem,\n FeedbackListParams,\n OrganizationConfig,\n Roadmap,\n} from \"./types\";\n\n// ============================================\n// Generic query hook — single source of truth\n// ============================================\n\nfunction useRefletQuery<T>(\n fetchFn: () => Promise<T>,\n enabled = true\n): UseQueryResult<T> {\n const [data, setData] = useState<T | undefined>();\n const [isLoading, setIsLoading] = useState(enabled);\n const [error, setError] = useState<Error | null>(null);\n\n const refetch = useCallback(async () => {\n setIsLoading(true);\n setError(null);\n try {\n const result = await fetchFn();\n setData(result);\n } catch (err) {\n setError(err instanceof Error ? err : new Error(\"Request failed\"));\n } finally {\n setIsLoading(false);\n }\n }, [fetchFn]);\n\n useEffect(() => {\n if (enabled) {\n refetch();\n }\n }, [refetch, enabled]);\n\n return { data, error, isLoading, refetch };\n}\n\n// ============================================\n// Organization Config Hook\n// ============================================\n\nexport function useOrganizationConfig(): UseQueryResult<OrganizationConfig> {\n const client = useRefletClient();\n const fetchFn = useCallback(() => client.getConfig(), [client]);\n return useRefletQuery(fetchFn);\n}\n\n// ============================================\n// Feedback List Hook\n// ============================================\n\nexport interface UseFeedbackListOptions extends FeedbackListParams {\n enabled?: boolean;\n}\n\nexport function useFeedbackList(\n options: UseFeedbackListOptions = {}\n): UseQueryResult<{ items: FeedbackItem[]; total: number; hasMore: boolean }> {\n const client = useRefletClient();\n const {\n enabled = true,\n statusId,\n status,\n search,\n sortBy,\n limit,\n offset,\n } = options;\n\n const params = useMemo(\n () => ({ limit, offset, search, sortBy, status, statusId }),\n [statusId, status, search, sortBy, limit, offset]\n );\n\n const fetchFn = useCallback(() => client.list(params), [client, params]);\n return useRefletQuery(fetchFn, enabled);\n}\n\n// ============================================\n// Single Feedback Hook\n// ============================================\n\nexport function useFeedback(\n feedbackId: string | undefined\n): UseQueryResult<FeedbackDetail> {\n const client = useRefletClient();\n\n const fetchFn = useCallback(() => {\n if (!feedbackId) {\n return Promise.resolve(undefined as unknown as FeedbackDetail);\n }\n return client.get(feedbackId);\n }, [client, feedbackId]);\n\n return useRefletQuery(fetchFn, !!feedbackId);\n}\n\n// ============================================\n// Comments Hook\n// ============================================\n\nexport function useComments(\n feedbackId: string | undefined,\n sortBy: \"newest\" | \"oldest\" = \"oldest\"\n): UseQueryResult<Comment[]> {\n const client = useRefletClient();\n\n const fetchFn = useCallback(() => {\n if (!feedbackId) {\n return Promise.resolve(undefined as unknown as Comment[]);\n }\n return client.getComments(feedbackId, sortBy);\n }, [client, feedbackId, sortBy]);\n\n return useRefletQuery(fetchFn, !!feedbackId);\n}\n\n// ============================================\n// Roadmap Hook\n// ============================================\n\nexport function useRoadmap(): UseQueryResult<Roadmap> {\n const client = useRefletClient();\n const fetchFn = useCallback(() => client.getRoadmap(), [client]);\n return useRefletQuery(fetchFn);\n}\n\n// ============================================\n// Changelog Hook\n// ============================================\n\nexport function useChangelog(limit?: number): UseQueryResult<ChangelogEntry[]> {\n const client = useRefletClient();\n const fetchFn = useCallback(\n () => client.getChangelog(limit),\n [client, limit]\n );\n return useRefletQuery(fetchFn);\n}\n\n// ============================================\n// Unread Changelog Count Hook\n// ============================================\n\nconst CHANGELOG_STORAGE_KEY_PREFIX = \"reflet_changelog_seen_\";\n\nfunction subscribeToStorage(onStoreChange: () => void): () => void {\n window.addEventListener(\"storage\", onStoreChange);\n return () => window.removeEventListener(\"storage\", onStoreChange);\n}\n\nfunction readLastSeen(storageKey: string): number {\n try {\n const stored = localStorage.getItem(storageKey);\n return stored ? Number(stored) : 0;\n } catch {\n return 0;\n }\n}\n\nexport function useUnreadChangelogCount(publicKey: string): {\n unreadCount: number;\n markAsRead: () => void;\n} {\n const { data: entries } = useChangelog();\n const [readMark, setReadMark] = useState<{\n storageKey: string;\n timestamp: number;\n } | null>(null);\n\n const storageKey = `${CHANGELOG_STORAGE_KEY_PREFIX}${publicKey}`;\n const storedLastSeen = useSyncExternalStore(\n subscribeToStorage,\n () => readLastSeen(storageKey),\n () => 0\n );\n const markedLastSeen =\n readMark?.storageKey === storageKey ? readMark.timestamp : 0;\n const lastSeen = Math.max(storedLastSeen, markedLastSeen);\n\n let unreadCount = 0;\n for (const entry of entries ?? []) {\n if (entry.publishedAt && entry.publishedAt > lastSeen) {\n unreadCount++;\n }\n }\n\n const markAsRead = useCallback(() => {\n if (!entries || entries.length === 0) {\n return;\n }\n\n let latestTimestamp = 0;\n for (const entry of entries) {\n if (entry.publishedAt && entry.publishedAt > latestTimestamp) {\n latestTimestamp = entry.publishedAt;\n }\n }\n\n if (latestTimestamp > 0) {\n try {\n localStorage.setItem(storageKey, String(latestTimestamp));\n } catch {\n // localStorage unavailable\n }\n setReadMark({ storageKey, timestamp: latestTimestamp });\n }\n }, [entries, storageKey]);\n\n return { markAsRead, unreadCount };\n}\n"],"mappings":"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"}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { n as Reflet } from "./client-
|
|
2
|
-
import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
|
1
|
+
import { n as Reflet } from "./client-DAdWdIFY.js";
|
|
2
|
+
import { createContext, useContext, useEffect, useMemo, useState, useSyncExternalStore } from "react";
|
|
3
3
|
import { jsx } from "react/jsx-runtime";
|
|
4
4
|
//#region src/react-context.tsx
|
|
5
5
|
const RefletContext = createContext(null);
|
|
6
|
+
function subscribeToNothing() {
|
|
7
|
+
return () => void 0;
|
|
8
|
+
}
|
|
6
9
|
/**
|
|
7
10
|
* Provider component for Reflet SDK
|
|
8
11
|
*
|
|
@@ -24,7 +27,7 @@ const RefletContext = createContext(null);
|
|
|
24
27
|
*/
|
|
25
28
|
function RefletProvider({ publicKey, baseUrl, user: initialUser, userToken, children }) {
|
|
26
29
|
const [user, setUser] = useState(initialUser);
|
|
27
|
-
const
|
|
30
|
+
const isReady = useSyncExternalStore(subscribeToNothing, () => true, () => false);
|
|
28
31
|
const client = useMemo(() => {
|
|
29
32
|
return new Reflet({
|
|
30
33
|
baseUrl,
|
|
@@ -46,9 +49,6 @@ function RefletProvider({ publicKey, baseUrl, user: initialUser, userToken, chil
|
|
|
46
49
|
user,
|
|
47
50
|
userToken
|
|
48
51
|
]);
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
setIsReady(true);
|
|
51
|
-
}, []);
|
|
52
52
|
const value = useMemo(() => ({
|
|
53
53
|
baseUrl,
|
|
54
54
|
client,
|
|
@@ -92,4 +92,4 @@ const REFLET_Z_INDEX = {
|
|
|
92
92
|
//#endregion
|
|
93
93
|
export { useRefletContext as a, useRefletClient as i, RefletContext as n, RefletProvider as r, REFLET_Z_INDEX as t };
|
|
94
94
|
|
|
95
|
-
//# sourceMappingURL=z-index-
|
|
95
|
+
//# sourceMappingURL=z-index-BL0SGZEP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"z-index-BL0SGZEP.js","names":[],"sources":["../src/react-context.tsx","../src/z-index.ts"],"sourcesContent":["import {\n createContext,\n type ReactNode,\n useContext,\n useEffect,\n useMemo,\n useState,\n useSyncExternalStore,\n} from \"react\";\nimport { Reflet } from \"./client\";\nimport type { RefletConfig, RefletUser } from \"./types\";\n\n// ============================================\n// Context Types\n// ============================================\n\nexport interface RefletContextValue {\n baseUrl?: string;\n client: Reflet;\n isReady: boolean;\n publicKey: string;\n setUser: (user: RefletUser | undefined) => void;\n user: RefletUser | undefined;\n userToken: string | undefined;\n}\n\n// ============================================\n// Context\n// ============================================\n\nexport const RefletContext = createContext<RefletContextValue | null>(null);\n\n// ============================================\n// Provider\n// ============================================\n\nexport interface RefletProviderProps {\n /** API base URL (optional, defaults to Reflet production API) */\n baseUrl?: string;\n /** Children */\n children: ReactNode;\n /** Your organization's public API key */\n publicKey: string;\n /** Initial user identification */\n user?: RefletUser;\n /** Pre-signed user token (alternative to user) */\n userToken?: string;\n}\n\nfunction subscribeToNothing(): () => void {\n return () => undefined;\n}\n\n/**\n * Provider component for Reflet SDK\n *\n * @example\n * ```tsx\n * import { RefletProvider } from 'reflet-sdk/react';\n *\n * function App() {\n * return (\n * <RefletProvider\n * publicKey=\"fb_pub_xxx\"\n * user={{ id: 'user_123', email: 'user@example.com', name: 'John' }}\n * >\n * <YourApp />\n * </RefletProvider>\n * );\n * }\n * ```\n */\nexport function RefletProvider({\n publicKey,\n baseUrl,\n user: initialUser,\n userToken,\n children,\n}: RefletProviderProps) {\n const [user, setUser] = useState<RefletUser | undefined>(initialUser);\n const isReady = useSyncExternalStore(\n subscribeToNothing,\n () => true,\n () => false\n );\n\n const client = useMemo(() => {\n const config: RefletConfig = {\n baseUrl,\n publicKey,\n user,\n userToken,\n };\n return new Reflet(config);\n }, [publicKey, baseUrl, user, userToken]);\n\n // Update client when user changes\n useEffect(() => {\n if (userToken) {\n client.setUserToken(userToken);\n } else {\n client.setUser(user);\n }\n }, [client, user, userToken]);\n\n const value = useMemo(\n () => ({\n baseUrl,\n client,\n isReady,\n publicKey,\n setUser,\n user,\n userToken,\n }),\n [client, publicKey, baseUrl, isReady, user, userToken]\n );\n\n return (\n <RefletContext.Provider value={value}>{children}</RefletContext.Provider>\n );\n}\n\n// ============================================\n// Hook to access context\n// ============================================\n\nexport function useRefletContext(): RefletContextValue {\n const context = useContext(RefletContext);\n if (!context) {\n throw new Error(\"useRefletContext must be used within a RefletProvider\");\n }\n return context;\n}\n\nexport function useRefletClient(): Reflet {\n const { client } = useRefletContext();\n return client;\n}\n","export const REFLET_Z_INDEX = {\n annotatorOverlay: 2_147_483_001,\n captureHalo: 2_147_482_999,\n devtoolsBar: 2_147_483_647,\n dialogContent: 2_147_482_991,\n dialogOverlay: 2_147_482_990,\n widgetRoot: 2_147_483_000,\n} as const;\n"],"mappings":";;;;AA8BA,MAAa,gBAAgB,cAAyC,IAAI;AAmB1E,SAAS,qBAAiC;CACxC,aAAa,KAAA;AACf;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,eAAe,EAC7B,WACA,SACA,MAAM,aACN,WACA,YACsB;CACtB,MAAM,CAAC,MAAM,WAAW,SAAiC,WAAW;CACpE,MAAM,UAAU,qBACd,0BACM,YACA,KACR;CAEA,MAAM,SAAS,cAAc;EAO3B,OAAO,IAAI,OAAO;GALhB;GACA;GACA;GACA;EAEqB,CAAC;CAC1B,GAAG;EAAC;EAAW;EAAS;EAAM;CAAS,CAAC;CAGxC,gBAAgB;EACd,IAAI,WACF,OAAO,aAAa,SAAS;OAE7B,OAAO,QAAQ,IAAI;CAEvB,GAAG;EAAC;EAAQ;EAAM;CAAS,CAAC;CAE5B,MAAM,QAAQ,eACL;EACL;EACA;EACA;EACA;EACA;EACA;EACA;CACF,IACA;EAAC;EAAQ;EAAW;EAAS;EAAS;EAAM;CAAS,CACvD;CAEA,OACE,oBAAC,cAAc,UAAf;EAA+B;EAAQ;CAAiC,CAAA;AAE5E;AAMA,SAAgB,mBAAuC;CACrD,MAAM,UAAU,WAAW,aAAa;CACxC,IAAI,CAAC,SACH,MAAM,IAAI,MAAM,uDAAuD;CAEzE,OAAO;AACT;AAEA,SAAgB,kBAA0B;CACxC,MAAM,EAAE,WAAW,iBAAiB;CACpC,OAAO;AACT;;;AC1IA,MAAa,iBAAiB;CAC5B,kBAAkB;CAClB,aAAa;CACb,aAAa;CACb,eAAe;CACf,eAAe;CACf,YAAY;AACd"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reflet-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Official Reflet SDK - Collect user feedback, feature requests, and bug reports from your app",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"feedback",
|
|
@@ -78,7 +78,8 @@
|
|
|
78
78
|
},
|
|
79
79
|
"dependencies": {
|
|
80
80
|
"@base-ui/react": "^1.8.0",
|
|
81
|
-
"@zumer/snapdom": "^3.0.0"
|
|
81
|
+
"@zumer/snapdom": "^3.0.0",
|
|
82
|
+
"zod": "^4.6.5"
|
|
82
83
|
},
|
|
83
84
|
"devDependencies": {
|
|
84
85
|
"@testing-library/dom": "^10.4.2",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"client-BWEBWk1k.js","names":[],"sources":["../src/types.ts","../src/client.ts"],"sourcesContent":["/**\n * Reflet SDK Types\n */\n\n// ============================================\n// Configuration\n// ============================================\n\nexport interface RefletConfig {\n /** API base URL (optional, defaults to Reflet production API) */\n baseUrl?: string;\n /** Your organization's public API key (fb_pub_xxx) */\n publicKey: string;\n /** User identification for SSO */\n user?: RefletUser;\n /** Pre-signed user token (alternative to user object) */\n userToken?: string;\n}\n\nexport interface RefletUser {\n /** URL to user's avatar image */\n avatar?: string;\n /** User's email address */\n email?: string;\n /** Unique identifier for the user in your system */\n id: string;\n /** Additional metadata */\n metadata?: Record<string, unknown>;\n /** User's display name */\n name?: string;\n}\n\n// ============================================\n// API Responses\n// ============================================\n\nexport interface OrganizationConfig {\n description?: string;\n id: string;\n isPublic: boolean;\n logo?: string;\n name: string;\n primaryColor?: string;\n settings?: OrganizationSettings;\n slug: string;\n statuses: OrganizationStatus[];\n}\n\nexport interface OrganizationSettings {\n allowAnonymousVoting?: boolean;\n defaultStatus?: FeedbackStatus;\n requireApproval?: boolean;\n}\n\nexport interface OrganizationStatus {\n color: string;\n icon?: string;\n id: string;\n name: string;\n order: number;\n}\n\nexport type FeedbackStatus =\n | \"open\"\n | \"under_review\"\n | \"planned\"\n | \"in_progress\"\n | \"completed\"\n | \"closed\";\n\n// ============================================\n// Feedback\n// ============================================\n\nexport interface FeedbackItem {\n author: FeedbackAuthor | null;\n commentCount: number;\n completedAt?: number;\n createdAt: number;\n description: string;\n hasVoted: boolean;\n id: string;\n isPinned: boolean;\n organizationStatus: {\n id: string;\n name: string;\n color: string;\n } | null;\n status: FeedbackStatus;\n tags: FeedbackTag[];\n title: string;\n updatedAt: number;\n voteCount: number;\n}\n\nexport interface FeedbackDetail extends FeedbackItem {\n isSubscribed: boolean;\n}\n\nexport interface FeedbackTag {\n color: string;\n id: string;\n name: string;\n}\n\nexport interface FeedbackAuthor {\n avatar?: string;\n email?: string;\n isExternal: boolean;\n name?: string;\n}\n\nexport interface FeedbackListResponse {\n hasMore: boolean;\n items: FeedbackItem[];\n total: number;\n}\n\nexport interface FeedbackListParams {\n /** Number of items to return (max 100) */\n limit?: number;\n /** Offset for pagination */\n offset?: number;\n /** Search in title and description */\n search?: string;\n /** Sort order */\n sortBy?: \"votes\" | \"newest\" | \"oldest\" | \"comments\";\n /** Filter by status name */\n status?: FeedbackStatus;\n /** Filter by custom status ID */\n statusId?: string;\n}\n\nexport interface ElementRect {\n height: number;\n width: number;\n x: number;\n y: number;\n}\n\nexport interface ElementSelection {\n /** What the reporter wrote about this element while picking it. */\n comment?: string;\n /** React component names owning the element, innermost first. */\n componentStack: string[];\n /** Truncated, redacted outerHTML of the selected element. */\n html: string;\n /** Short human-readable description, e.g. `button \"Sign in\"`. */\n label: string;\n rect: ElementRect;\n /** Page area holding the element, e.g. `dialog \"Members\" › Danger zone`. */\n region?: string;\n selector: string;\n /** `src/components/login-form.tsx:46:19` when the React build exposes it. */\n sourceLocation?: string;\n /** Collapsed visible text of the element — survives markup truncation. */\n text?: string;\n}\n\nexport interface ConsoleEvent {\n level: \"error\" | \"warn\";\n message: string;\n timestamp: number;\n}\n\nexport interface PageContext {\n browser?: string;\n device?: string;\n language?: string;\n os?: string;\n pageTitle?: string;\n referrer?: string;\n screen?: { height: number; width: number };\n /** Where the viewport sat in the page when the screenshot was taken. */\n scroll?: { x: number; y: number };\n timezone?: string;\n url?: string;\n userAgent?: string;\n viewport?: { devicePixelRatio: number; height: number; width: number };\n}\n\n/** Everything the widget knows about where and how the feedback was written. */\nexport interface FeedbackContext extends PageContext {\n consoleEvents?: ConsoleEvent[];\n metadata?: Record<string, string>;\n sdkVersion?: string;\n selections?: ElementSelection[];\n}\n\nexport interface CreateFeedbackParams {\n context?: FeedbackContext;\n description: string;\n title: string;\n}\n\nexport interface ScreenshotAnnotation {\n color?: string;\n endX?: number;\n endY?: number;\n height?: number;\n points?: Array<{ x: number; y: number }>;\n text?: string;\n type:\n | \"arrow\"\n | \"blur\"\n | \"highlight\"\n | \"pen\"\n | \"rectangle\"\n | \"spotlight\"\n | \"text\";\n width?: number;\n x: number;\n y: number;\n}\n\nexport interface SaveScreenshotParams {\n annotatedStorageId?: string;\n annotations?: ScreenshotAnnotation[];\n /** `element` for the close-up of a picked element, `widget` for the page. */\n captureSource?: \"element\" | \"widget\";\n feedbackId: string;\n filename?: string;\n height?: number;\n mimeType?: string;\n pageUrl?: string;\n size?: number;\n storageId: string;\n width?: number;\n}\n\nexport interface CreateFeedbackResponse {\n feedbackId: string;\n isApproved: boolean;\n}\n\nexport interface VoteResponse {\n voteCount: number;\n voted: boolean;\n}\n\n// ============================================\n// Comments\n// ============================================\n\nexport interface Comment {\n author: FeedbackAuthor | null;\n body: string;\n createdAt: number;\n id: string;\n isOfficial: boolean;\n replies: CommentReply[];\n updatedAt: number;\n}\n\nexport interface CommentReply {\n author: FeedbackAuthor | null;\n body: string;\n createdAt: number;\n id: string;\n isOfficial: boolean;\n updatedAt: number;\n}\n\nexport interface AddCommentParams {\n body: string;\n feedbackId: string;\n parentId?: string;\n}\n\nexport interface AddCommentResponse {\n commentId: string;\n}\n\n// ============================================\n// Subscriptions\n// ============================================\n\nexport interface SubscribeResponse {\n alreadySubscribed?: boolean;\n subscribed: boolean;\n}\n\nexport interface UnsubscribeResponse {\n unsubscribed: boolean;\n wasNotSubscribed?: boolean;\n}\n\n// ============================================\n// Roadmap\n// ============================================\n\nexport interface Roadmap {\n lanes: RoadmapLane[];\n}\n\nexport interface RoadmapLane {\n color: string;\n id: string;\n items: RoadmapItem[];\n name: string;\n}\n\nexport interface RoadmapItem {\n id: string;\n status: FeedbackStatus;\n title: string;\n voteCount: number;\n}\n\n// ============================================\n// Changelog\n// ============================================\n\nexport interface ChangelogEntry {\n description?: string;\n feedback: { id: string; title: string }[];\n id: string;\n publishedAt?: number;\n title: string;\n version?: string;\n}\n\n// ============================================\n// Errors\n// ============================================\n\nexport class RefletError extends Error {\n readonly status: number;\n readonly code?: string;\n\n constructor(message: string, status: number, code?: string) {\n super(message);\n this.name = \"RefletError\";\n this.status = status;\n this.code = code;\n }\n}\n\nexport class RefletAuthError extends RefletError {\n constructor(message: string) {\n super(message, 401, \"AUTH_ERROR\");\n this.name = \"RefletAuthError\";\n }\n}\n\nexport class RefletValidationError extends RefletError {\n constructor(message: string) {\n super(message, 400, \"VALIDATION_ERROR\");\n this.name = \"RefletValidationError\";\n }\n}\n\nexport class RefletNotFoundError extends RefletError {\n constructor(message: string) {\n super(message, 404, \"NOT_FOUND\");\n this.name = \"RefletNotFoundError\";\n }\n}\n","import {\n type AddCommentParams,\n type AddCommentResponse,\n type ChangelogEntry,\n type Comment,\n type CreateFeedbackParams,\n type CreateFeedbackResponse,\n type FeedbackDetail,\n type FeedbackListParams,\n type FeedbackListResponse,\n type OrganizationConfig,\n RefletAuthError,\n type RefletConfig,\n RefletError,\n RefletNotFoundError,\n type RefletUser,\n type Roadmap,\n type SaveScreenshotParams,\n type SubscribeResponse,\n type UnsubscribeResponse,\n type VoteResponse,\n} from \"./types\";\n\nexport const DEFAULT_API_URL = \"https://harmless-clam-802.convex.site\";\n\n/** btoa is Latin1-only — a name like \"José\" would throw. */\nfunction base64Utf8(value: string): string {\n let binary = \"\";\n for (const byte of new TextEncoder().encode(value)) {\n binary += String.fromCharCode(byte);\n }\n return btoa(binary);\n}\n\n/**\n * Reflet SDK Client\n *\n * @example\n * ```ts\n * import { Reflet } from 'reflet-sdk';\n *\n * const reflet = new Reflet({\n * publicKey: 'fb_pub_xxx',\n * user: { id: 'user_123', email: 'user@example.com', name: 'John' }\n * });\n *\n * // List feedback\n * const { items } = await reflet.list({ status: 'open' });\n *\n * // Vote on feedback\n * await reflet.vote('feedback_id');\n *\n * // Submit new feedback\n * await reflet.create({ title: 'Great idea', description: 'Details...' });\n * ```\n */\nexport class Reflet {\n private readonly publicKey: string;\n private readonly baseUrl: string;\n private userToken: string | undefined;\n private user: RefletUser | undefined;\n\n constructor(config: RefletConfig) {\n this.publicKey = config.publicKey;\n this.baseUrl = config.baseUrl ?? DEFAULT_API_URL;\n this.userToken = config.userToken;\n this.user = config.user;\n\n if (!this.publicKey) {\n throw new Error(\"Reflet: publicKey is required\");\n }\n }\n\n /**\n * Update the user identification\n */\n setUser(user: RefletUser | undefined): void {\n this.user = user;\n this.userToken = undefined;\n }\n\n /**\n * Set the user token directly (for server-signed tokens)\n */\n setUserToken(token: string | undefined): void {\n this.userToken = token;\n this.user = undefined;\n }\n\n /**\n * Get the organization configuration\n */\n async getConfig(): Promise<OrganizationConfig> {\n return await this.request<OrganizationConfig>(\"GET\", \"/api/v1/feedback\");\n }\n\n /**\n * List feedback items with optional filtering\n */\n async list(params?: FeedbackListParams): Promise<FeedbackListResponse> {\n const searchParams = new URLSearchParams();\n\n if (params?.statusId) {\n searchParams.set(\"statusId\", params.statusId);\n }\n if (params?.status) {\n searchParams.set(\"status\", params.status);\n }\n if (params?.search) {\n searchParams.set(\"search\", params.search);\n }\n if (params?.sortBy) {\n searchParams.set(\"sortBy\", params.sortBy);\n }\n if (params?.limit) {\n searchParams.set(\"limit\", String(params.limit));\n }\n if (params?.offset) {\n searchParams.set(\"offset\", String(params.offset));\n }\n\n const query = searchParams.toString();\n const url = `/api/v1/feedback/list${query ? `?${query}` : \"\"}`;\n\n return await this.request<FeedbackListResponse>(\"GET\", url);\n }\n\n /**\n * Get a single feedback item by ID\n */\n async get(feedbackId: string): Promise<FeedbackDetail> {\n const result = await this.request<FeedbackDetail | null>(\n \"GET\",\n `/api/v1/feedback/item?id=${encodeURIComponent(feedbackId)}`\n );\n\n if (!result) {\n throw new RefletNotFoundError(\"Feedback not found\");\n }\n\n return result;\n }\n\n /**\n * Create new feedback\n * Works anonymously or with user identification\n */\n async create(params: CreateFeedbackParams): Promise<CreateFeedbackResponse> {\n return await this.request<CreateFeedbackResponse>(\n \"POST\",\n \"/api/v1/feedback/create\",\n params\n );\n }\n\n /**\n * Ask the API for a one-shot storage URL to upload a screenshot to.\n */\n async getScreenshotUploadUrl(): Promise<{ uploadUrl: string }> {\n return await this.request<{ uploadUrl: string }>(\n \"POST\",\n \"/api/v1/feedback/screenshot/upload-url\"\n );\n }\n\n /**\n * Attach an already uploaded screenshot to a feedback item.\n */\n async saveScreenshot(\n params: SaveScreenshotParams\n ): Promise<{ screenshotId: string }> {\n return await this.request<{ screenshotId: string }>(\n \"POST\",\n \"/api/v1/feedback/screenshot/save\",\n params\n );\n }\n\n /**\n * Toggle vote on feedback\n * Requires user identification\n */\n async vote(\n feedbackId: string,\n type: \"upvote\" | \"downvote\" = \"upvote\"\n ): Promise<VoteResponse> {\n return await this.request<VoteResponse>(\"POST\", \"/api/v1/feedback/vote\", {\n feedbackId,\n voteType: type,\n });\n }\n\n /**\n * Get comments for a feedback item\n */\n async getComments(\n feedbackId: string,\n sortBy: \"newest\" | \"oldest\" = \"oldest\"\n ): Promise<Comment[]> {\n return await this.request<Comment[]>(\n \"GET\",\n `/api/v1/feedback/comments?feedbackId=${encodeURIComponent(feedbackId)}&sortBy=${sortBy}`\n );\n }\n\n /**\n * Add a comment to feedback\n * Requires user identification\n */\n async comment(params: AddCommentParams): Promise<AddCommentResponse> {\n return await this.request<AddCommentResponse>(\n \"POST\",\n \"/api/v1/feedback/comment\",\n params\n );\n }\n\n /**\n * Subscribe to feedback updates\n * Requires user identification\n */\n async subscribe(feedbackId: string): Promise<SubscribeResponse> {\n return await this.request<SubscribeResponse>(\n \"POST\",\n \"/api/v1/feedback/subscribe\",\n {\n feedbackId,\n }\n );\n }\n\n /**\n * Unsubscribe from feedback updates\n * Requires user identification\n */\n async unsubscribe(feedbackId: string): Promise<UnsubscribeResponse> {\n return await this.request<UnsubscribeResponse>(\n \"POST\",\n \"/api/v1/feedback/unsubscribe\",\n {\n feedbackId,\n }\n );\n }\n\n /**\n * Get roadmap data\n */\n async getRoadmap(): Promise<Roadmap> {\n return await this.request<Roadmap>(\"GET\", \"/api/v1/feedback/roadmap\");\n }\n\n /**\n * Get changelog entries\n */\n async getChangelog(limit?: number): Promise<ChangelogEntry[]> {\n const url = limit\n ? `/api/v1/feedback/changelog?limit=${limit}`\n : \"/api/v1/feedback/changelog\";\n\n return await this.request<ChangelogEntry[]>(\"GET\", url);\n }\n\n /**\n * Build request headers with authentication\n */\n private buildHeaders(): Record<string, string> {\n const headers: Record<string, string> = {\n Authorization: `Bearer ${this.publicKey}`,\n \"Content-Type\": \"application/json\",\n };\n\n const token = this.userToken ?? this.generateUserToken();\n if (token) {\n headers[\"X-User-Token\"] = token;\n }\n\n return headers;\n }\n\n /**\n * Parse JSON response text safely.\n * Returns `unknown` — callers must narrow before use.\n */\n private parseJsonSafely(text: string, status: number): unknown {\n try {\n return JSON.parse(text);\n } catch (parseError) {\n throw new RefletError(\n `Invalid response: ${parseError instanceof Error ? parseError.message : \"Failed to parse JSON\"}`,\n status\n );\n }\n }\n\n /**\n * Type guard for API error responses\n */\n private isErrorResponse(data: unknown): data is { error: string } {\n return (\n typeof data === \"object\" &&\n data !== null &&\n \"error\" in data &&\n typeof (data as Record<string, unknown>).error === \"string\"\n );\n }\n\n /**\n * Throw appropriate error based on status code\n */\n private throwHttpError(message: string, status: number): never {\n if (status === 401) {\n throw new RefletAuthError(message);\n }\n if (status === 404) {\n throw new RefletNotFoundError(message);\n }\n throw new RefletError(message, status);\n }\n\n /**\n * Make an authenticated API request\n */\n private async request<T>(\n method: string,\n path: string,\n body?: unknown\n ): Promise<T> {\n let response: Response;\n try {\n response = await fetch(`${this.baseUrl}${path}`, {\n body: body ? JSON.stringify(body) : undefined,\n headers: this.buildHeaders(),\n method,\n });\n } catch (networkError) {\n const message =\n networkError instanceof Error\n ? networkError.message\n : \"Failed to connect\";\n throw new RefletError(`Network error: ${message}`, 0);\n }\n\n const text = await response.text();\n\n // Handle empty response\n if (!text) {\n if (!response.ok) {\n this.throwHttpError(\n `Request failed with status ${response.status}`,\n response.status\n );\n }\n // Successful empty body (e.g. 204 No Content) — return empty object\n // narrowed through unknown for type-safe assertion\n const emptyResponse: unknown = {};\n return emptyResponse as T;\n }\n\n const data = this.parseJsonSafely(text, response.status);\n\n if (!response.ok) {\n const errorMessage = this.isErrorResponse(data)\n ? data.error\n : `Request failed with status ${response.status}`;\n this.throwHttpError(errorMessage, response.status);\n }\n\n // Standard unknown → T assertion for runtime-parsed JSON\n return data as T;\n }\n\n /**\n * Unsigned identity from the `user` option. The API attributes reports with\n * it but refuses voting, commenting and subscribing — those need `signUser`.\n */\n private generateUserToken(): string | undefined {\n if (!this.user) {\n return;\n }\n\n const payload = {\n email: this.user.email,\n exp: Math.floor(Date.now() / 1000) + 86_400, // 24 hours\n iat: Math.floor(Date.now() / 1000),\n id: this.user.id,\n name: this.user.name,\n };\n\n const header = base64Utf8(JSON.stringify({ alg: \"none\", typ: \"JWT\" }));\n const payloadB64 = base64Utf8(JSON.stringify(payload));\n\n // Empty signature — the server treats this identity as unverified\n return `${header}.${payloadB64}.`;\n }\n}\n"],"mappings":";AAsUA,IAAa,cAAb,cAAiC,MAAM;CACrC;CACA;CAEA,YAAY,SAAiB,QAAgB,MAAe;EAC1D,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,SAAS;EACd,KAAK,OAAO;CACd;AACF;AAEA,IAAa,kBAAb,cAAqC,YAAY;CAC/C,YAAY,SAAiB;EAC3B,MAAM,SAAS,KAAK,YAAY;EAChC,KAAK,OAAO;CACd;AACF;AAEA,IAAa,wBAAb,cAA2C,YAAY;CACrD,YAAY,SAAiB;EAC3B,MAAM,SAAS,KAAK,kBAAkB;EACtC,KAAK,OAAO;CACd;AACF;AAEA,IAAa,sBAAb,cAAyC,YAAY;CACnD,YAAY,SAAiB;EAC3B,MAAM,SAAS,KAAK,WAAW;EAC/B,KAAK,OAAO;CACd;AACF;;;AC9UA,MAAa,kBAAkB;;AAG/B,SAAS,WAAW,OAAuB;CACzC,IAAI,SAAS;CACb,KAAK,MAAM,QAAQ,IAAI,YAAY,CAAC,CAAC,OAAO,KAAK,GAC/C,UAAU,OAAO,aAAa,IAAI;CAEpC,OAAO,KAAK,MAAM;AACpB;;;;;;;;;;;;;;;;;;;;;;;AAwBA,IAAa,SAAb,MAAoB;CAClB;CACA;CACA;CACA;CAEA,YAAY,QAAsB;EAChC,KAAK,YAAY,OAAO;EACxB,KAAK,UAAU,OAAO,WAAA;EACtB,KAAK,YAAY,OAAO;EACxB,KAAK,OAAO,OAAO;EAEnB,IAAI,CAAC,KAAK,WACR,MAAM,IAAI,MAAM,+BAA+B;CAEnD;;;;CAKA,QAAQ,MAAoC;EAC1C,KAAK,OAAO;EACZ,KAAK,YAAY,KAAA;CACnB;;;;CAKA,aAAa,OAAiC;EAC5C,KAAK,YAAY;EACjB,KAAK,OAAO,KAAA;CACd;;;;CAKA,MAAM,YAAyC;EAC7C,OAAO,MAAM,KAAK,QAA4B,OAAO,kBAAkB;CACzE;;;;CAKA,MAAM,KAAK,QAA4D;EACrE,MAAM,eAAe,IAAI,gBAAgB;EAEzC,IAAI,QAAQ,UACV,aAAa,IAAI,YAAY,OAAO,QAAQ;EAE9C,IAAI,QAAQ,QACV,aAAa,IAAI,UAAU,OAAO,MAAM;EAE1C,IAAI,QAAQ,QACV,aAAa,IAAI,UAAU,OAAO,MAAM;EAE1C,IAAI,QAAQ,QACV,aAAa,IAAI,UAAU,OAAO,MAAM;EAE1C,IAAI,QAAQ,OACV,aAAa,IAAI,SAAS,OAAO,OAAO,KAAK,CAAC;EAEhD,IAAI,QAAQ,QACV,aAAa,IAAI,UAAU,OAAO,OAAO,MAAM,CAAC;EAGlD,MAAM,QAAQ,aAAa,SAAS;EACpC,MAAM,MAAM,wBAAwB,QAAQ,IAAI,UAAU;EAE1D,OAAO,MAAM,KAAK,QAA8B,OAAO,GAAG;CAC5D;;;;CAKA,MAAM,IAAI,YAA6C;EACrD,MAAM,SAAS,MAAM,KAAK,QACxB,OACA,4BAA4B,mBAAmB,UAAU,GAC3D;EAEA,IAAI,CAAC,QACH,MAAM,IAAI,oBAAoB,oBAAoB;EAGpD,OAAO;CACT;;;;;CAMA,MAAM,OAAO,QAA+D;EAC1E,OAAO,MAAM,KAAK,QAChB,QACA,2BACA,MACF;CACF;;;;CAKA,MAAM,yBAAyD;EAC7D,OAAO,MAAM,KAAK,QAChB,QACA,wCACF;CACF;;;;CAKA,MAAM,eACJ,QACmC;EACnC,OAAO,MAAM,KAAK,QAChB,QACA,oCACA,MACF;CACF;;;;;CAMA,MAAM,KACJ,YACA,OAA8B,UACP;EACvB,OAAO,MAAM,KAAK,QAAsB,QAAQ,yBAAyB;GACvE;GACA,UAAU;EACZ,CAAC;CACH;;;;CAKA,MAAM,YACJ,YACA,SAA8B,UACV;EACpB,OAAO,MAAM,KAAK,QAChB,OACA,wCAAwC,mBAAmB,UAAU,EAAE,UAAU,QACnF;CACF;;;;;CAMA,MAAM,QAAQ,QAAuD;EACnE,OAAO,MAAM,KAAK,QAChB,QACA,4BACA,MACF;CACF;;;;;CAMA,MAAM,UAAU,YAAgD;EAC9D,OAAO,MAAM,KAAK,QAChB,QACA,8BACA,EACE,WACF,CACF;CACF;;;;;CAMA,MAAM,YAAY,YAAkD;EAClE,OAAO,MAAM,KAAK,QAChB,QACA,gCACA,EACE,WACF,CACF;CACF;;;;CAKA,MAAM,aAA+B;EACnC,OAAO,MAAM,KAAK,QAAiB,OAAO,0BAA0B;CACtE;;;;CAKA,MAAM,aAAa,OAA2C;EAC5D,MAAM,MAAM,QACR,oCAAoC,UACpC;EAEJ,OAAO,MAAM,KAAK,QAA0B,OAAO,GAAG;CACxD;;;;CAKA,eAA+C;EAC7C,MAAM,UAAkC;GACtC,eAAe,UAAU,KAAK;GAC9B,gBAAgB;EAClB;EAEA,MAAM,QAAQ,KAAK,aAAa,KAAK,kBAAkB;EACvD,IAAI,OACF,QAAQ,kBAAkB;EAG5B,OAAO;CACT;;;;;CAMA,gBAAwB,MAAc,QAAyB;EAC7D,IAAI;GACF,OAAO,KAAK,MAAM,IAAI;EACxB,SAAS,YAAY;GACnB,MAAM,IAAI,YACR,qBAAqB,sBAAsB,QAAQ,WAAW,UAAU,0BACxE,MACF;EACF;CACF;;;;CAKA,gBAAwB,MAA0C;EAChE,OACE,OAAO,SAAS,YAChB,SAAS,QACT,WAAW,QACX,OAAQ,KAAiC,UAAU;CAEvD;;;;CAKA,eAAuB,SAAiB,QAAuB;EAC7D,IAAI,WAAW,KACb,MAAM,IAAI,gBAAgB,OAAO;EAEnC,IAAI,WAAW,KACb,MAAM,IAAI,oBAAoB,OAAO;EAEvC,MAAM,IAAI,YAAY,SAAS,MAAM;CACvC;;;;CAKA,MAAc,QACZ,QACA,MACA,MACY;EACZ,IAAI;EACJ,IAAI;GACF,WAAW,MAAM,MAAM,GAAG,KAAK,UAAU,QAAQ;IAC/C,MAAM,OAAO,KAAK,UAAU,IAAI,IAAI,KAAA;IACpC,SAAS,KAAK,aAAa;IAC3B;GACF,CAAC;EACH,SAAS,cAAc;GAKrB,MAAM,IAAI,YAAY,kBAHpB,wBAAwB,QACpB,aAAa,UACb,uBAC6C,CAAC;EACtD;EAEA,MAAM,OAAO,MAAM,SAAS,KAAK;EAGjC,IAAI,CAAC,MAAM;GACT,IAAI,CAAC,SAAS,IACZ,KAAK,eACH,8BAA8B,SAAS,UACvC,SAAS,MACX;GAKF,OAAO,CAAY;EACrB;EAEA,MAAM,OAAO,KAAK,gBAAgB,MAAM,SAAS,MAAM;EAEvD,IAAI,CAAC,SAAS,IAAI;GAChB,MAAM,eAAe,KAAK,gBAAgB,IAAI,IAC1C,KAAK,QACL,8BAA8B,SAAS;GAC3C,KAAK,eAAe,cAAc,SAAS,MAAM;EACnD;EAGA,OAAO;CACT;;;;;CAMA,oBAAgD;EAC9C,IAAI,CAAC,KAAK,MACR;EAGF,MAAM,UAAU;GACd,OAAO,KAAK,KAAK;GACjB,KAAK,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,IAAI;GACrC,KAAK,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;GACjC,IAAI,KAAK,KAAK;GACd,MAAM,KAAK,KAAK;EAClB;EAMA,OAAO,GAJQ,WAAW,KAAK,UAAU;GAAE,KAAK;GAAQ,KAAK;EAAM,CAAC,CAIrD,EAAE,GAHE,WAAW,KAAK,UAAU,OAAO,CAGvB,EAAE;CACjC;AACF"}
|