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/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
+ });