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