fdbck-react 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +145 -0
- package/dist/index.css +418 -0
- package/dist/index.d.mts +115 -0
- package/dist/index.d.ts +115 -0
- package/dist/index.js +716 -0
- package/dist/index.mjs +688 -0
- package/package.json +52 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
21
|
+
|
|
22
|
+
// src/index.ts
|
|
23
|
+
var index_exports = {};
|
|
24
|
+
__export(index_exports, {
|
|
25
|
+
FdbckError: () => FdbckError,
|
|
26
|
+
FdbckProvider: () => FdbckProvider,
|
|
27
|
+
FdbckWidget: () => FdbckWidget,
|
|
28
|
+
useFdbck: () => useFdbck
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(index_exports);
|
|
31
|
+
|
|
32
|
+
// src/components/FdbckWidget.tsx
|
|
33
|
+
var import_react7 = require("react");
|
|
34
|
+
|
|
35
|
+
// src/lib/errors.ts
|
|
36
|
+
var FdbckError = class extends Error {
|
|
37
|
+
constructor(code, message) {
|
|
38
|
+
super(message);
|
|
39
|
+
__publicField(this, "code");
|
|
40
|
+
this.name = "FdbckError";
|
|
41
|
+
this.code = code;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
function statusToErrorCode(status) {
|
|
45
|
+
switch (status) {
|
|
46
|
+
case "invalid":
|
|
47
|
+
case "token_expired":
|
|
48
|
+
return "invalid_token";
|
|
49
|
+
case "already_responded":
|
|
50
|
+
return "already_responded";
|
|
51
|
+
case "question_ended":
|
|
52
|
+
return "question_expired";
|
|
53
|
+
default:
|
|
54
|
+
return "unknown";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function httpStatusToErrorCode(status) {
|
|
58
|
+
if (status === 401) return "invalid_token";
|
|
59
|
+
if (status === 409) return "already_responded";
|
|
60
|
+
if (status === 410) return "question_expired";
|
|
61
|
+
return "unknown";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/lib/api.ts
|
|
65
|
+
var DEFAULT_BASE_URL = "https://api.fdbck.sh";
|
|
66
|
+
var FdbckApiClient = class {
|
|
67
|
+
constructor(baseUrl) {
|
|
68
|
+
__publicField(this, "baseUrl");
|
|
69
|
+
this.baseUrl = (baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
70
|
+
}
|
|
71
|
+
/** Fetch question data for a token via GET /v1/f/:token */
|
|
72
|
+
async fetchQuestion(token, signal) {
|
|
73
|
+
let res;
|
|
74
|
+
try {
|
|
75
|
+
res = await fetch(`${this.baseUrl}/v1/f/${encodeURIComponent(token)}`, { signal });
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (err.name === "AbortError") throw err;
|
|
78
|
+
throw new FdbckError("network_error", "Failed to fetch question");
|
|
79
|
+
}
|
|
80
|
+
if (!res.ok) {
|
|
81
|
+
throw new FdbckError(httpStatusToErrorCode(res.status), `HTTP ${res.status}`);
|
|
82
|
+
}
|
|
83
|
+
const data = await res.json();
|
|
84
|
+
if (data.status !== "valid") {
|
|
85
|
+
throw new FdbckError(statusToErrorCode(data.status), `Token status: ${data.status}`);
|
|
86
|
+
}
|
|
87
|
+
return data;
|
|
88
|
+
}
|
|
89
|
+
/** Submit a response via POST /v1/responses */
|
|
90
|
+
async submitResponse(token, value, signal) {
|
|
91
|
+
let res;
|
|
92
|
+
try {
|
|
93
|
+
res = await fetch(`${this.baseUrl}/v1/responses`, {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: { "Content-Type": "application/json" },
|
|
96
|
+
body: JSON.stringify({ token, value }),
|
|
97
|
+
signal
|
|
98
|
+
});
|
|
99
|
+
} catch (err) {
|
|
100
|
+
if (err.name === "AbortError") throw err;
|
|
101
|
+
throw new FdbckError("network_error", "Failed to submit response");
|
|
102
|
+
}
|
|
103
|
+
if (!res.ok) {
|
|
104
|
+
const body = await res.json().catch(() => null);
|
|
105
|
+
const message = body?.error?.message || `HTTP ${res.status}`;
|
|
106
|
+
throw new FdbckError(httpStatusToErrorCode(res.status), message);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// src/lib/colors.ts
|
|
112
|
+
function getThemeVars(themeColor, themeMode) {
|
|
113
|
+
const accent = themeColor || "#FF6B35";
|
|
114
|
+
const isDark = themeMode === "dark";
|
|
115
|
+
return {
|
|
116
|
+
"--fdbck-accent": accent,
|
|
117
|
+
"--fdbck-accent-alpha": `${accent}18`,
|
|
118
|
+
"--fdbck-bg": isDark ? "#0a0a0a" : "#f9fafb",
|
|
119
|
+
"--fdbck-card-bg": isDark ? "#141414" : "#ffffff",
|
|
120
|
+
"--fdbck-border": isDark ? "#262626" : "#e5e7eb",
|
|
121
|
+
"--fdbck-text-primary": isDark ? "#ffffff" : "#111827",
|
|
122
|
+
"--fdbck-text-secondary": isDark ? "#9ca3af" : "#6b7280",
|
|
123
|
+
"--fdbck-text-muted": isDark ? "#6b7280" : "#9ca3af",
|
|
124
|
+
"--fdbck-indicator-border": isDark ? "#52525b" : "#d1d5db"
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/lib/style.ts
|
|
129
|
+
function getStyleVars(style) {
|
|
130
|
+
return {
|
|
131
|
+
"--fdbck-width": style?.width || "100%",
|
|
132
|
+
"--fdbck-max-width": style?.maxWidth || "28rem",
|
|
133
|
+
"--fdbck-border-radius": style?.borderRadius || "0.75rem",
|
|
134
|
+
"--fdbck-font-family": style?.fontFamily || "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
135
|
+
"--fdbck-font-size": style?.fontSize || "0.875rem"
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/lib/locale.ts
|
|
140
|
+
var DEFAULT_LOCALE = {
|
|
141
|
+
submit: "Submit",
|
|
142
|
+
poweredBy: "Powered by",
|
|
143
|
+
loading: "Loading\u2026",
|
|
144
|
+
errorTitle: "Something went wrong",
|
|
145
|
+
errorMessage: "Unable to load this question. Please try again later.",
|
|
146
|
+
retry: "Retry",
|
|
147
|
+
close: "Close"
|
|
148
|
+
};
|
|
149
|
+
function resolveLocale(locale) {
|
|
150
|
+
if (!locale) return DEFAULT_LOCALE;
|
|
151
|
+
return { ...DEFAULT_LOCALE, ...locale };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/hooks/useWidgetState.ts
|
|
155
|
+
var import_react = require("react");
|
|
156
|
+
function reducer(state, action) {
|
|
157
|
+
switch (action.type) {
|
|
158
|
+
case "TOKEN_RESOLVED":
|
|
159
|
+
return { ...state, status: "loading", token: action.token };
|
|
160
|
+
case "LOADED":
|
|
161
|
+
return { ...state, status: "ready", question: action.question, token: action.token };
|
|
162
|
+
case "SUBMITTING":
|
|
163
|
+
return { ...state, status: "submitting" };
|
|
164
|
+
case "COMPLETE":
|
|
165
|
+
return { ...state, status: "complete", submittedValue: action.value };
|
|
166
|
+
case "ERROR":
|
|
167
|
+
return { ...state, status: "error", error: action.error };
|
|
168
|
+
case "RESET":
|
|
169
|
+
return initialState;
|
|
170
|
+
default:
|
|
171
|
+
return state;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
var initialState = {
|
|
175
|
+
status: "loading",
|
|
176
|
+
question: null,
|
|
177
|
+
token: null,
|
|
178
|
+
error: null,
|
|
179
|
+
submittedValue: null
|
|
180
|
+
};
|
|
181
|
+
var fetchingTokenState = {
|
|
182
|
+
...initialState,
|
|
183
|
+
status: "fetching_token"
|
|
184
|
+
};
|
|
185
|
+
function useWidgetState(needsTokenFetch) {
|
|
186
|
+
return (0, import_react.useReducer)(reducer, needsTokenFetch ? fetchingTokenState : initialState);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// src/hooks/useFocusTrap.ts
|
|
190
|
+
var import_react2 = require("react");
|
|
191
|
+
var FOCUSABLE = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
192
|
+
function useFocusTrap(containerRef, active) {
|
|
193
|
+
(0, import_react2.useEffect)(() => {
|
|
194
|
+
if (!active) return;
|
|
195
|
+
const container = containerRef.current;
|
|
196
|
+
if (!container) return;
|
|
197
|
+
function handleKeyDown(e) {
|
|
198
|
+
if (e.key !== "Tab") return;
|
|
199
|
+
const focusable = Array.from(container.querySelectorAll(FOCUSABLE));
|
|
200
|
+
if (focusable.length === 0) {
|
|
201
|
+
e.preventDefault();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const first = focusable[0];
|
|
205
|
+
const last = focusable[focusable.length - 1];
|
|
206
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
207
|
+
e.preventDefault();
|
|
208
|
+
last.focus();
|
|
209
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
210
|
+
e.preventDefault();
|
|
211
|
+
first.focus();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
container.addEventListener("keydown", handleKeyDown);
|
|
215
|
+
const firstFocusable = container.querySelector(FOCUSABLE);
|
|
216
|
+
firstFocusable?.focus();
|
|
217
|
+
return () => container.removeEventListener("keydown", handleKeyDown);
|
|
218
|
+
}, [containerRef, active]);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// src/components/FdbckShadowHost.tsx
|
|
222
|
+
var import_react3 = require("react");
|
|
223
|
+
var import_react_dom = require("react-dom");
|
|
224
|
+
|
|
225
|
+
// src/styles/base.css
|
|
226
|
+
var base_default = {};
|
|
227
|
+
|
|
228
|
+
// src/styles/question-types.css
|
|
229
|
+
var question_types_default = {};
|
|
230
|
+
|
|
231
|
+
// src/styles/confirmation.css
|
|
232
|
+
var confirmation_default = {};
|
|
233
|
+
|
|
234
|
+
// src/styles/branding.css
|
|
235
|
+
var branding_default = {};
|
|
236
|
+
|
|
237
|
+
// src/styles/modal.css
|
|
238
|
+
var modal_default = {};
|
|
239
|
+
|
|
240
|
+
// src/styles/popover.css
|
|
241
|
+
var popover_default = {};
|
|
242
|
+
|
|
243
|
+
// src/styles/index.ts
|
|
244
|
+
var WIDGET_CSS = [
|
|
245
|
+
base_default,
|
|
246
|
+
question_types_default,
|
|
247
|
+
confirmation_default,
|
|
248
|
+
branding_default,
|
|
249
|
+
modal_default,
|
|
250
|
+
popover_default
|
|
251
|
+
].join("\n");
|
|
252
|
+
|
|
253
|
+
// src/components/FdbckShadowHost.tsx
|
|
254
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
255
|
+
function FdbckShadowHost({ children, style }) {
|
|
256
|
+
const hostRef = (0, import_react3.useRef)(null);
|
|
257
|
+
const [container, setContainer] = (0, import_react3.useState)(null);
|
|
258
|
+
(0, import_react3.useEffect)(() => {
|
|
259
|
+
const host = hostRef.current;
|
|
260
|
+
if (!host || host.shadowRoot) return;
|
|
261
|
+
const shadow = host.attachShadow({ mode: "open" });
|
|
262
|
+
const styleEl = document.createElement("style");
|
|
263
|
+
styleEl.textContent = WIDGET_CSS;
|
|
264
|
+
shadow.appendChild(styleEl);
|
|
265
|
+
const wrapper = document.createElement("div");
|
|
266
|
+
shadow.appendChild(wrapper);
|
|
267
|
+
setContainer(wrapper);
|
|
268
|
+
}, []);
|
|
269
|
+
(0, import_react3.useEffect)(() => {
|
|
270
|
+
if (!container || !style) return;
|
|
271
|
+
const parent = container.parentNode;
|
|
272
|
+
const host = parent?.host;
|
|
273
|
+
if (host) {
|
|
274
|
+
for (const [key, value] of Object.entries(style)) {
|
|
275
|
+
host.style.setProperty(key, value);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}, [container, style]);
|
|
279
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ref: hostRef, children: container && (0, import_react_dom.createPortal)(children, container) });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/components/FdbckLoading.tsx
|
|
283
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
284
|
+
function FdbckLoading() {
|
|
285
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "fdbck-loading", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "fdbck-spinner" }) });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// src/components/FdbckConfirmation.tsx
|
|
289
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
290
|
+
function FdbckConfirmation({ thankYouMessage }) {
|
|
291
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fdbck-confirmation", children: [
|
|
292
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "fdbck-check-circle", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2.5, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M5 13l4 4L19 7" }) }) }),
|
|
293
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { children: thankYouMessage ? "Thank you" : "Response recorded" }),
|
|
294
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { children: thankYouMessage || "Your response has been recorded. You can close this." })
|
|
295
|
+
] });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// src/components/FdbckError.tsx
|
|
299
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
300
|
+
var ERROR_TITLES = {
|
|
301
|
+
invalid_token: "Invalid link",
|
|
302
|
+
already_responded: "Already responded",
|
|
303
|
+
question_expired: "Question closed"
|
|
304
|
+
};
|
|
305
|
+
var ERROR_MESSAGES = {
|
|
306
|
+
invalid_token: "This response link is not valid.",
|
|
307
|
+
already_responded: "You've already responded to this.",
|
|
308
|
+
question_expired: "This question has closed."
|
|
309
|
+
};
|
|
310
|
+
function FdbckErrorDisplay({ error, locale, onRetry }) {
|
|
311
|
+
const title = ERROR_TITLES[error.code] || locale.errorTitle;
|
|
312
|
+
const message = ERROR_MESSAGES[error.code] || locale.errorMessage;
|
|
313
|
+
const isRetryable = error.code === "network_error" || error.code === "unknown";
|
|
314
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "fdbck-error-state", children: [
|
|
315
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h2", { children: title }),
|
|
316
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { children: message }),
|
|
317
|
+
isRetryable && onRetry && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "fdbck-retry-btn", onClick: onRetry, children: locale.retry })
|
|
318
|
+
] });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/components/FdbckHeader.tsx
|
|
322
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
323
|
+
function FdbckHeader({ question }) {
|
|
324
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
|
|
325
|
+
question.welcome_message && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "fdbck-welcome", children: question.welcome_message }),
|
|
326
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("h1", { className: "fdbck-question", children: question.question })
|
|
327
|
+
] });
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// src/components/questions/YesNoQuestion.tsx
|
|
331
|
+
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
332
|
+
function YesNoQuestion({ options, disabled, onSubmit }) {
|
|
333
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "fdbck-yesno-grid", children: options.map((opt) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
334
|
+
"button",
|
|
335
|
+
{
|
|
336
|
+
className: "fdbck-option-btn",
|
|
337
|
+
disabled,
|
|
338
|
+
onClick: () => onSubmit(opt),
|
|
339
|
+
children: opt
|
|
340
|
+
},
|
|
341
|
+
opt
|
|
342
|
+
)) });
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// src/components/questions/SingleChoiceQuestion.tsx
|
|
346
|
+
var import_react4 = require("react");
|
|
347
|
+
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
348
|
+
function SingleChoiceQuestion({ options, disabled, onSubmit }) {
|
|
349
|
+
const [selected, setSelected] = (0, import_react4.useState)(null);
|
|
350
|
+
function handleClick(opt) {
|
|
351
|
+
setSelected(opt);
|
|
352
|
+
onSubmit(opt);
|
|
353
|
+
}
|
|
354
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "fdbck-choice-list", children: options.map((opt) => {
|
|
355
|
+
const isSelected = selected === opt;
|
|
356
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
357
|
+
"button",
|
|
358
|
+
{
|
|
359
|
+
className: "fdbck-option-btn",
|
|
360
|
+
"data-selected": isSelected,
|
|
361
|
+
disabled,
|
|
362
|
+
onClick: () => handleClick(opt),
|
|
363
|
+
children: [
|
|
364
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "fdbck-radio", "data-selected": isSelected, children: isSelected && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "fdbck-radio-dot" }) }),
|
|
365
|
+
opt
|
|
366
|
+
]
|
|
367
|
+
},
|
|
368
|
+
opt
|
|
369
|
+
);
|
|
370
|
+
}) });
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/components/questions/MultipleChoiceQuestion.tsx
|
|
374
|
+
var import_react5 = require("react");
|
|
375
|
+
var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
376
|
+
function MultipleChoiceQuestion({ options, disabled, locale, onSubmit }) {
|
|
377
|
+
const [selected, setSelected] = (0, import_react5.useState)([]);
|
|
378
|
+
function toggle(opt) {
|
|
379
|
+
setSelected(
|
|
380
|
+
(prev) => prev.includes(opt) ? prev.filter((x) => x !== opt) : [...prev, opt]
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "fdbck-choice-list", children: [
|
|
384
|
+
options.map((opt) => {
|
|
385
|
+
const isSelected = selected.includes(opt);
|
|
386
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
|
|
387
|
+
"button",
|
|
388
|
+
{
|
|
389
|
+
className: "fdbck-option-btn",
|
|
390
|
+
"data-selected": isSelected,
|
|
391
|
+
disabled,
|
|
392
|
+
onClick: () => toggle(opt),
|
|
393
|
+
children: [
|
|
394
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "fdbck-checkbox", "data-selected": isSelected, children: isSelected && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("svg", { fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 3, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M5 13l4 4L19 7" }) }) }),
|
|
395
|
+
opt
|
|
396
|
+
]
|
|
397
|
+
},
|
|
398
|
+
opt
|
|
399
|
+
);
|
|
400
|
+
}),
|
|
401
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
402
|
+
"button",
|
|
403
|
+
{
|
|
404
|
+
className: "fdbck-submit-btn",
|
|
405
|
+
disabled: disabled || selected.length === 0,
|
|
406
|
+
onClick: () => onSubmit(selected),
|
|
407
|
+
children: locale.submit
|
|
408
|
+
}
|
|
409
|
+
)
|
|
410
|
+
] });
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// src/components/questions/RatingQuestion.tsx
|
|
414
|
+
var import_react6 = require("react");
|
|
415
|
+
var import_jsx_runtime9 = require("react/jsx-runtime");
|
|
416
|
+
function RatingQuestion({ options, disabled, onSubmit }) {
|
|
417
|
+
const [selected, setSelected] = (0, import_react6.useState)(null);
|
|
418
|
+
const count = options.max - options.min + 1;
|
|
419
|
+
const columns = Math.min(count, 10);
|
|
420
|
+
function handleClick(v) {
|
|
421
|
+
setSelected(v);
|
|
422
|
+
onSubmit(v);
|
|
423
|
+
}
|
|
424
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { children: [
|
|
425
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "fdbck-rating-grid", style: { gridTemplateColumns: `repeat(${columns}, 1fr)` }, children: Array.from({ length: count }, (_, i) => options.min + i).map((v) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
426
|
+
"button",
|
|
427
|
+
{
|
|
428
|
+
className: "fdbck-rating-btn",
|
|
429
|
+
"data-selected": selected === v,
|
|
430
|
+
disabled,
|
|
431
|
+
onClick: () => handleClick(v),
|
|
432
|
+
children: v
|
|
433
|
+
},
|
|
434
|
+
v
|
|
435
|
+
)) }),
|
|
436
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "fdbck-rating-labels", children: [
|
|
437
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { children: options.min_label || options.min }),
|
|
438
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { children: options.max_label || options.max })
|
|
439
|
+
] })
|
|
440
|
+
] });
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// src/components/FdbckBody.tsx
|
|
444
|
+
var import_jsx_runtime10 = require("react/jsx-runtime");
|
|
445
|
+
function FdbckBody({ question, disabled, locale, onSubmit }) {
|
|
446
|
+
switch (question.type) {
|
|
447
|
+
case "yes_no":
|
|
448
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(YesNoQuestion, { options: question.options, disabled, onSubmit });
|
|
449
|
+
case "single_choice":
|
|
450
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(SingleChoiceQuestion, { options: question.options, disabled, onSubmit });
|
|
451
|
+
case "multiple_choice":
|
|
452
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(MultipleChoiceQuestion, { options: question.options, disabled, locale, onSubmit });
|
|
453
|
+
case "rating":
|
|
454
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(RatingQuestion, { options: question.options, disabled, onSubmit });
|
|
455
|
+
default:
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/components/FdbckFooter.tsx
|
|
461
|
+
var import_jsx_runtime11 = require("react/jsx-runtime");
|
|
462
|
+
function FdbckFooter({ hideBranding, locale }) {
|
|
463
|
+
if (hideBranding) return null;
|
|
464
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "fdbck-branding", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("a", { href: "https://fdbck.sh", target: "_blank", rel: "noopener noreferrer", children: [
|
|
465
|
+
locale.poweredBy,
|
|
466
|
+
" ",
|
|
467
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "fdbck-branding-name", children: "fdbck" })
|
|
468
|
+
] }) });
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// src/components/FdbckCard.tsx
|
|
472
|
+
var import_jsx_runtime12 = require("react/jsx-runtime");
|
|
473
|
+
function FdbckCard({ state, locale, submitError, onSubmit, onRetry }) {
|
|
474
|
+
const { status, question, error } = state;
|
|
475
|
+
if (status === "fetching_token" || status === "loading") {
|
|
476
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "fdbck-card", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(FdbckLoading, {}) });
|
|
477
|
+
}
|
|
478
|
+
if (status === "error" && error) {
|
|
479
|
+
const showBranding = error.code !== "invalid_token" && error.code !== "unknown" && !!question;
|
|
480
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "fdbck-card", children: [
|
|
481
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(FdbckErrorDisplay, { error, locale, onRetry }),
|
|
482
|
+
showBranding && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(FdbckFooter, { hideBranding: question?.hide_branding ?? true, locale })
|
|
483
|
+
] });
|
|
484
|
+
}
|
|
485
|
+
if (status === "complete" && question) {
|
|
486
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "fdbck-card", children: [
|
|
487
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(FdbckConfirmation, { thankYouMessage: question.thank_you_message }),
|
|
488
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(FdbckFooter, { hideBranding: question.hide_branding, locale })
|
|
489
|
+
] });
|
|
490
|
+
}
|
|
491
|
+
if ((status === "ready" || status === "submitting") && question) {
|
|
492
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "fdbck-card", children: [
|
|
493
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(FdbckHeader, { question }),
|
|
494
|
+
submitError && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "fdbck-error-text", children: submitError }),
|
|
495
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
496
|
+
FdbckBody,
|
|
497
|
+
{
|
|
498
|
+
question,
|
|
499
|
+
disabled: status === "submitting",
|
|
500
|
+
locale,
|
|
501
|
+
onSubmit
|
|
502
|
+
}
|
|
503
|
+
),
|
|
504
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(FdbckFooter, { hideBranding: question.hide_branding, locale })
|
|
505
|
+
] });
|
|
506
|
+
}
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// src/components/FdbckWidget.tsx
|
|
511
|
+
var import_jsx_runtime13 = require("react/jsx-runtime");
|
|
512
|
+
function FdbckWidget({
|
|
513
|
+
token,
|
|
514
|
+
mode = "inline",
|
|
515
|
+
open = true,
|
|
516
|
+
baseUrl,
|
|
517
|
+
delay = 0,
|
|
518
|
+
autoCloseAfter,
|
|
519
|
+
closeOnOverlayClick = true,
|
|
520
|
+
closeOnEscape = true,
|
|
521
|
+
locale: localeProp,
|
|
522
|
+
style: styleProp,
|
|
523
|
+
onSubmit,
|
|
524
|
+
onDismiss,
|
|
525
|
+
onError,
|
|
526
|
+
onLoad
|
|
527
|
+
}) {
|
|
528
|
+
const [state, dispatch] = useWidgetState(false);
|
|
529
|
+
const [submitError, setSubmitError] = (0, import_react7.useState)(null);
|
|
530
|
+
const [visible, setVisible] = (0, import_react7.useState)(delay === 0);
|
|
531
|
+
const clientRef = (0, import_react7.useRef)(new FdbckApiClient(baseUrl));
|
|
532
|
+
const submitRef = (0, import_react7.useRef)(false);
|
|
533
|
+
const modalRef = (0, import_react7.useRef)(null);
|
|
534
|
+
const locale = resolveLocale(localeProp);
|
|
535
|
+
(0, import_react7.useEffect)(() => {
|
|
536
|
+
if (delay <= 0 || visible) return;
|
|
537
|
+
const timer = setTimeout(() => setVisible(true), delay);
|
|
538
|
+
return () => clearTimeout(timer);
|
|
539
|
+
}, [delay, visible]);
|
|
540
|
+
const loadQuestion = (0, import_react7.useCallback)(() => {
|
|
541
|
+
const controller = new AbortController();
|
|
542
|
+
clientRef.current.fetchQuestion(token, controller.signal).then((data) => {
|
|
543
|
+
dispatch({ type: "LOADED", question: data.question, token: data.token });
|
|
544
|
+
onLoad?.(data.question);
|
|
545
|
+
}).catch((err) => {
|
|
546
|
+
if (err.name === "AbortError") return;
|
|
547
|
+
const fdbckErr = err instanceof FdbckError ? err : new FdbckError("unknown", err.message);
|
|
548
|
+
dispatch({ type: "ERROR", error: fdbckErr });
|
|
549
|
+
onError?.(fdbckErr);
|
|
550
|
+
});
|
|
551
|
+
return () => controller.abort();
|
|
552
|
+
}, [token, onLoad, onError]);
|
|
553
|
+
(0, import_react7.useEffect)(() => {
|
|
554
|
+
return loadQuestion();
|
|
555
|
+
}, [loadQuestion]);
|
|
556
|
+
function handleSubmit(value) {
|
|
557
|
+
if (submitRef.current) return;
|
|
558
|
+
submitRef.current = true;
|
|
559
|
+
setSubmitError(null);
|
|
560
|
+
dispatch({ type: "SUBMITTING" });
|
|
561
|
+
clientRef.current.submitResponse(state.token || token, value).then(() => {
|
|
562
|
+
dispatch({ type: "COMPLETE", value });
|
|
563
|
+
onSubmit?.(value);
|
|
564
|
+
if (autoCloseAfter && autoCloseAfter > 0) {
|
|
565
|
+
setTimeout(() => onDismiss?.(), autoCloseAfter);
|
|
566
|
+
}
|
|
567
|
+
}).catch((err) => {
|
|
568
|
+
const msg = err instanceof FdbckError ? err.message : "Something went wrong";
|
|
569
|
+
setSubmitError(msg);
|
|
570
|
+
dispatch({ type: "LOADED", question: state.question, token: state.token });
|
|
571
|
+
submitRef.current = false;
|
|
572
|
+
onError?.(err instanceof FdbckError ? err : new FdbckError("unknown", String(err)));
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
function handleRetry() {
|
|
576
|
+
dispatch({ type: "RESET" });
|
|
577
|
+
submitRef.current = false;
|
|
578
|
+
setSubmitError(null);
|
|
579
|
+
loadQuestion();
|
|
580
|
+
}
|
|
581
|
+
(0, import_react7.useEffect)(() => {
|
|
582
|
+
if (mode === "inline" || !closeOnEscape || !open) return;
|
|
583
|
+
function handleKeyDown(e) {
|
|
584
|
+
if (e.key === "Escape") onDismiss?.();
|
|
585
|
+
}
|
|
586
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
587
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
588
|
+
}, [mode, closeOnEscape, open, onDismiss]);
|
|
589
|
+
useFocusTrap(modalRef, mode === "modal" && open && visible);
|
|
590
|
+
if (!open || !visible) return null;
|
|
591
|
+
const themeVars = getThemeVars(state.question?.theme_color ?? null, state.question?.theme_mode ?? "dark");
|
|
592
|
+
const styleVars = getStyleVars(styleProp);
|
|
593
|
+
const cssVars = { ...themeVars, ...styleVars };
|
|
594
|
+
const card = /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
595
|
+
FdbckCard,
|
|
596
|
+
{
|
|
597
|
+
state,
|
|
598
|
+
locale,
|
|
599
|
+
submitError,
|
|
600
|
+
onSubmit: handleSubmit,
|
|
601
|
+
onRetry: handleRetry
|
|
602
|
+
}
|
|
603
|
+
);
|
|
604
|
+
if (mode === "modal") {
|
|
605
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(FdbckShadowHost, { style: cssVars, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
606
|
+
"div",
|
|
607
|
+
{
|
|
608
|
+
className: "fdbck-modal-backdrop",
|
|
609
|
+
onClick: closeOnOverlayClick ? onDismiss : void 0,
|
|
610
|
+
role: "dialog",
|
|
611
|
+
"aria-modal": "true",
|
|
612
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
|
|
613
|
+
"div",
|
|
614
|
+
{
|
|
615
|
+
ref: modalRef,
|
|
616
|
+
className: "fdbck-modal-content",
|
|
617
|
+
onClick: (e) => e.stopPropagation(),
|
|
618
|
+
children: [
|
|
619
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("button", { className: "fdbck-close-btn", onClick: onDismiss, "aria-label": locale.close, children: "\xD7" }),
|
|
620
|
+
card
|
|
621
|
+
]
|
|
622
|
+
}
|
|
623
|
+
)
|
|
624
|
+
}
|
|
625
|
+
) });
|
|
626
|
+
}
|
|
627
|
+
if (mode === "popover") {
|
|
628
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(FdbckShadowHost, { style: cssVars, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "fdbck-popover", role: "dialog", children: [
|
|
629
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("button", { className: "fdbck-close-btn", onClick: onDismiss, "aria-label": locale.close, children: "\xD7" }),
|
|
630
|
+
card
|
|
631
|
+
] }) });
|
|
632
|
+
}
|
|
633
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(FdbckShadowHost, { style: cssVars, children: card });
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// src/components/FdbckProvider.tsx
|
|
637
|
+
var import_react8 = require("react");
|
|
638
|
+
var import_jsx_runtime14 = require("react/jsx-runtime");
|
|
639
|
+
var FdbckContext = (0, import_react8.createContext)(null);
|
|
640
|
+
function FdbckProvider({ baseUrl, locale, style, children }) {
|
|
641
|
+
const [activeConfig, setActiveConfig] = (0, import_react8.useState)(null);
|
|
642
|
+
const resolverRef = (0, import_react8.useRef)(null);
|
|
643
|
+
const show = (0, import_react8.useCallback)((options) => {
|
|
644
|
+
return new Promise((resolve, reject) => {
|
|
645
|
+
resolverRef.current = { resolve, reject };
|
|
646
|
+
setActiveConfig(options);
|
|
647
|
+
});
|
|
648
|
+
}, []);
|
|
649
|
+
const dismiss = (0, import_react8.useCallback)(() => {
|
|
650
|
+
if (resolverRef.current) {
|
|
651
|
+
resolverRef.current.resolve({ status: "dismissed" });
|
|
652
|
+
resolverRef.current = null;
|
|
653
|
+
}
|
|
654
|
+
setActiveConfig(null);
|
|
655
|
+
}, []);
|
|
656
|
+
const handleSubmit = (0, import_react8.useCallback)((value) => {
|
|
657
|
+
if (resolverRef.current) {
|
|
658
|
+
resolverRef.current.resolve({ status: "submitted", value });
|
|
659
|
+
resolverRef.current = null;
|
|
660
|
+
}
|
|
661
|
+
setActiveConfig(null);
|
|
662
|
+
}, []);
|
|
663
|
+
const handleError = (0, import_react8.useCallback)((err) => {
|
|
664
|
+
activeConfig;
|
|
665
|
+
}, []);
|
|
666
|
+
const contextValue = {
|
|
667
|
+
show,
|
|
668
|
+
dismiss,
|
|
669
|
+
isActive: activeConfig !== null,
|
|
670
|
+
baseUrl,
|
|
671
|
+
locale,
|
|
672
|
+
style
|
|
673
|
+
};
|
|
674
|
+
return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(FdbckContext.Provider, { value: contextValue, children: [
|
|
675
|
+
children,
|
|
676
|
+
activeConfig && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
677
|
+
FdbckWidget,
|
|
678
|
+
{
|
|
679
|
+
token: activeConfig.token,
|
|
680
|
+
mode: activeConfig.mode || "modal",
|
|
681
|
+
open: activeConfig.open ?? true,
|
|
682
|
+
baseUrl: activeConfig.baseUrl || baseUrl,
|
|
683
|
+
delay: activeConfig.delay,
|
|
684
|
+
autoCloseAfter: activeConfig.autoCloseAfter,
|
|
685
|
+
closeOnOverlayClick: activeConfig.closeOnOverlayClick,
|
|
686
|
+
closeOnEscape: activeConfig.closeOnEscape,
|
|
687
|
+
locale: activeConfig.locale || locale,
|
|
688
|
+
style: activeConfig.style || style,
|
|
689
|
+
onSubmit: handleSubmit,
|
|
690
|
+
onDismiss: dismiss,
|
|
691
|
+
onError: handleError
|
|
692
|
+
}
|
|
693
|
+
)
|
|
694
|
+
] });
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// src/hooks/useFdbck.ts
|
|
698
|
+
var import_react9 = require("react");
|
|
699
|
+
function useFdbck() {
|
|
700
|
+
const context = (0, import_react9.useContext)(FdbckContext);
|
|
701
|
+
if (!context) {
|
|
702
|
+
throw new Error("useFdbck must be used within a <FdbckProvider>");
|
|
703
|
+
}
|
|
704
|
+
return {
|
|
705
|
+
show: context.show,
|
|
706
|
+
dismiss: context.dismiss,
|
|
707
|
+
isActive: context.isActive
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
711
|
+
0 && (module.exports = {
|
|
712
|
+
FdbckError,
|
|
713
|
+
FdbckProvider,
|
|
714
|
+
FdbckWidget,
|
|
715
|
+
useFdbck
|
|
716
|
+
});
|