@verbumia/feedback 0.1.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.
Files changed (42) hide show
  1. package/CONTRACT.md +165 -0
  2. package/LICENSE +21 -0
  3. package/README.md +80 -0
  4. package/dist/chunk-5NA2TFPG.js +1 -0
  5. package/dist/chunk-5NA2TFPG.js.map +1 -0
  6. package/dist/chunk-OX4RJD5H.js +242 -0
  7. package/dist/chunk-OX4RJD5H.js.map +1 -0
  8. package/dist/client-CPEcvn23.d.cts +159 -0
  9. package/dist/client-CPEcvn23.d.ts +159 -0
  10. package/dist/core/index.cjs +272 -0
  11. package/dist/core/index.cjs.map +1 -0
  12. package/dist/core/index.d.cts +18 -0
  13. package/dist/core/index.d.ts +18 -0
  14. package/dist/core/index.js +16 -0
  15. package/dist/core/index.js.map +1 -0
  16. package/dist/keys-BySe1O6V.d.ts +25 -0
  17. package/dist/keys-Dg_nv16u.d.cts +25 -0
  18. package/dist/native/index.cjs +575 -0
  19. package/dist/native/index.cjs.map +1 -0
  20. package/dist/native/index.d.cts +54 -0
  21. package/dist/native/index.d.ts +54 -0
  22. package/dist/native/index.js +322 -0
  23. package/dist/native/index.js.map +1 -0
  24. package/dist/react/index.cjs +644 -0
  25. package/dist/react/index.cjs.map +1 -0
  26. package/dist/react/index.d.cts +55 -0
  27. package/dist/react/index.d.ts +55 -0
  28. package/dist/react/index.js +384 -0
  29. package/dist/react/index.js.map +1 -0
  30. package/dist/svelte/index.cjs +306 -0
  31. package/dist/svelte/index.cjs.map +1 -0
  32. package/dist/svelte/index.d.cts +38 -0
  33. package/dist/svelte/index.d.ts +38 -0
  34. package/dist/svelte/index.js +52 -0
  35. package/dist/svelte/index.js.map +1 -0
  36. package/dist/vue/index.cjs +426 -0
  37. package/dist/vue/index.cjs.map +1 -0
  38. package/dist/vue/index.d.cts +39 -0
  39. package/dist/vue/index.d.ts +39 -0
  40. package/dist/vue/index.js +172 -0
  41. package/dist/vue/index.js.map +1 -0
  42. package/package.json +108 -0
@@ -0,0 +1,644 @@
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 __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/react/index.ts
21
+ var react_exports = {};
22
+ __export(react_exports, {
23
+ FeedbackClient: () => FeedbackClient,
24
+ FeedbackError: () => FeedbackError,
25
+ FeedbackPanel: () => FeedbackPanel,
26
+ feedbackPlugin: () => feedbackPlugin,
27
+ hasKeyRegistry: () => hasKeyRegistry,
28
+ resolveKeys: () => resolveKeys
29
+ });
30
+ module.exports = __toCommonJS(react_exports);
31
+
32
+ // src/react/plugin.tsx
33
+ var import_react_dom = require("react-dom");
34
+ var import_react2 = require("react");
35
+
36
+ // src/core/types.ts
37
+ var FeedbackError = class extends Error {
38
+ constructor(message, status, code) {
39
+ super(message);
40
+ this.status = status;
41
+ this.code = code;
42
+ this.name = "FeedbackError";
43
+ }
44
+ status;
45
+ code;
46
+ };
47
+
48
+ // src/core/tos.ts
49
+ var SDK_TOS_VERSION = "2026-05-18";
50
+
51
+ // src/core/client.ts
52
+ var SDK_LIB = "@verbumia/feedback";
53
+ var SDK_VER = "0.1.0";
54
+ var FeedbackClient = class {
55
+ cfg;
56
+ fetchImpl;
57
+ tokens = null;
58
+ queue = [];
59
+ timer = null;
60
+ bootstrapping = null;
61
+ constructor(config) {
62
+ this.cfg = {
63
+ flushDebounceMs: 1500,
64
+ maxBatch: 20,
65
+ ...config
66
+ };
67
+ const f = config.fetchImpl ?? globalThis.fetch;
68
+ if (!f) {
69
+ throw new FeedbackError(
70
+ "no fetch implementation available; pass config.fetchImpl"
71
+ );
72
+ }
73
+ this.fetchImpl = f.bind(globalThis);
74
+ }
75
+ base() {
76
+ return this.cfg.apiBase.replace(/\/+$/, "");
77
+ }
78
+ get endUserId() {
79
+ return this.tokens?.end_user_id ?? this.cfg.endUserId;
80
+ }
81
+ get hasConsented() {
82
+ return this.tokens !== null;
83
+ }
84
+ /** Server-minted sessionId / grouping_key (from the token bundle).
85
+ * Available only after `acceptTos()`; never client-generated. */
86
+ get sessionId() {
87
+ return this.tokens?.grouping_key;
88
+ }
89
+ /** BCP-47 language the widget is rating strings in. */
90
+ get language() {
91
+ return this.cfg.language;
92
+ }
93
+ /** ToS version the end user is asked to accept — the SDK's
94
+ * build-time constant (task 616). NOT integrator/server set. */
95
+ get tosVersion() {
96
+ return SDK_TOS_VERSION;
97
+ }
98
+ /**
99
+ * Accept the ToS and bootstrap a scoped token. Idempotent: a second call
100
+ * returns the in-flight / existing bundle rather than re-accepting.
101
+ */
102
+ async acceptTos() {
103
+ if (this.tokens) return this.tokens;
104
+ if (this.bootstrapping) return this.bootstrapping;
105
+ this.bootstrapping = (async () => {
106
+ const res = await this.fetchImpl(`${this.base()}/v1/feedback/tos`, {
107
+ method: "POST",
108
+ headers: { "Content-Type": "application/json" },
109
+ body: JSON.stringify({
110
+ // NO grouping_key: server-minted (task 599). It comes back in
111
+ // the token bundle and is bound into the JWT server-side.
112
+ project_id: this.cfg.projectId,
113
+ end_user_id: this.cfg.endUserId,
114
+ tos_version: SDK_TOS_VERSION,
115
+ locale: this.cfg.locale
116
+ })
117
+ });
118
+ if (!res.ok) throw await this.problem(res, "tos acceptance failed");
119
+ this.tokens = await res.json();
120
+ return this.tokens;
121
+ })();
122
+ try {
123
+ return await this.bootstrapping;
124
+ } finally {
125
+ this.bootstrapping = null;
126
+ }
127
+ }
128
+ async refresh() {
129
+ if (!this.tokens) throw new FeedbackError("not consented");
130
+ const res = await this.fetchImpl(
131
+ `${this.base()}/v1/feedback/token/refresh`,
132
+ {
133
+ method: "POST",
134
+ headers: { "Content-Type": "application/json" },
135
+ body: JSON.stringify({ refresh_token: this.tokens.refresh_token })
136
+ }
137
+ );
138
+ if (!res.ok) {
139
+ this.tokens = null;
140
+ throw await this.problem(res, "token refresh failed");
141
+ }
142
+ this.tokens = await res.json();
143
+ }
144
+ /** Authenticated fetch with a single transparent refresh-on-401 retry. */
145
+ async authed(path, init, retry = true) {
146
+ if (!this.tokens) await this.acceptTos();
147
+ const res = await this.fetchImpl(`${this.base()}${path}`, {
148
+ ...init,
149
+ headers: {
150
+ ...init.headers ?? {},
151
+ Authorization: `Bearer ${this.tokens.access_token}`
152
+ }
153
+ });
154
+ if (res.status === 401 && retry) {
155
+ await this.refresh();
156
+ return this.authed(path, init, false);
157
+ }
158
+ return res;
159
+ }
160
+ /** Strings rendered on the current view, with this end user's prior rating. */
161
+ async getStrings(opts) {
162
+ const qs = new URLSearchParams({ language: this.cfg.language });
163
+ if (opts?.keys?.length) {
164
+ qs.set("keys", opts.keys.map((k) => `${k.namespace}:${k.key}`).join(","));
165
+ }
166
+ if (opts?.namespace) qs.set("namespace", opts.namespace);
167
+ if (opts?.limit) qs.set("limit", String(opts.limit));
168
+ const res = await this.authed(`/v1/feedback/strings?${qs}`, {
169
+ method: "GET"
170
+ });
171
+ if (!res.ok) throw await this.problem(res, "failed to load strings");
172
+ return await res.json();
173
+ }
174
+ /** Queue a rating; flushed on debounce or when the batch fills. */
175
+ rate(payload) {
176
+ this.enqueue({ kind: "rating", payload });
177
+ }
178
+ /** Queue a suggestion; flushed on debounce or when the batch fills. */
179
+ suggest(payload) {
180
+ this.enqueue({ kind: "suggestion", payload });
181
+ }
182
+ enqueue(item) {
183
+ this.queue.push(item);
184
+ if (this.queue.length >= this.cfg.maxBatch) {
185
+ void this.flush();
186
+ return;
187
+ }
188
+ if (this.timer) clearTimeout(this.timer);
189
+ this.timer = setTimeout(() => void this.flush(), this.cfg.flushDebounceMs);
190
+ }
191
+ /**
192
+ * Flush queued items. Best-effort: a transport/auth failure re-queues the
193
+ * batch once and swallows the error so the host app never sees a throw.
194
+ */
195
+ async flush() {
196
+ if (this.timer) {
197
+ clearTimeout(this.timer);
198
+ this.timer = null;
199
+ }
200
+ if (!this.queue.length) return;
201
+ const batch = this.queue;
202
+ this.queue = [];
203
+ const ratings = batch.filter((b) => b.kind === "rating").map((b) => b.payload);
204
+ const suggestions = batch.filter((b) => b.kind === "suggestion").map((b) => b.payload);
205
+ try {
206
+ if (ratings.length) {
207
+ await this.postBatch("/v1/feedback/ratings", { ratings });
208
+ }
209
+ if (suggestions.length) {
210
+ await this.postBatch("/v1/feedback/suggestions", { suggestions });
211
+ }
212
+ } catch {
213
+ this.queue.unshift(...batch);
214
+ }
215
+ }
216
+ async postBatch(path, body) {
217
+ const res = await this.authed(path, {
218
+ method: "POST",
219
+ headers: { "Content-Type": "application/json", "X-SDK": `${SDK_LIB}@${SDK_VER}` },
220
+ body: JSON.stringify(body)
221
+ });
222
+ if (!res.ok) throw await this.problem(res, "batch post failed");
223
+ return await res.json();
224
+ }
225
+ async problem(res, fallback) {
226
+ let code;
227
+ let detail = fallback;
228
+ try {
229
+ const body = await res.json();
230
+ code = body.code;
231
+ if (body.detail) detail = body.detail;
232
+ } catch {
233
+ }
234
+ return new FeedbackError(detail, res.status, code);
235
+ }
236
+ };
237
+
238
+ // src/react/panel.tsx
239
+ var import_react = require("react");
240
+
241
+ // src/core/keys.ts
242
+ var REGISTRY_GLOBAL = "__verbumia_key_registry__";
243
+ function getRegistry() {
244
+ const g = globalThis;
245
+ const reg = g[REGISTRY_GLOBAL];
246
+ if (reg && typeof reg.snapshot === "function") {
247
+ return reg;
248
+ }
249
+ return null;
250
+ }
251
+ function hasKeyRegistry() {
252
+ return getRegistry() !== null;
253
+ }
254
+ function resolveKeys(explicit) {
255
+ if (explicit && explicit.length) return dedupe(explicit);
256
+ const reg = getRegistry();
257
+ if (reg) return dedupe(reg.snapshot());
258
+ return [];
259
+ }
260
+ function dedupe(keys) {
261
+ const seen = /* @__PURE__ */ new Set();
262
+ const out = [];
263
+ for (const k of keys) {
264
+ const id = `${k.namespace}:${k.key}`;
265
+ if (!seen.has(id)) {
266
+ seen.add(id);
267
+ out.push(k);
268
+ }
269
+ }
270
+ return out;
271
+ }
272
+
273
+ // src/react/panel.tsx
274
+ var import_jsx_runtime = require("react/jsx-runtime");
275
+ var C = {
276
+ bg: "#0b0f0e",
277
+ panel: "#111714",
278
+ border: "#1f2a25",
279
+ text: "#e7f5ef",
280
+ dim: "#8aa79b",
281
+ emerald: "#10b981",
282
+ emeraldSoft: "#34d399"
283
+ };
284
+ function FeedbackPanel(props) {
285
+ const { client, keys, onClose } = props;
286
+ const [consented, setConsented] = (0, import_react.useState)(client.hasConsented);
287
+ const [busy, setBusy] = (0, import_react.useState)(false);
288
+ const [error, setError] = (0, import_react.useState)(null);
289
+ const [strings, setStrings] = (0, import_react.useState)([]);
290
+ const loadStrings = (0, import_react.useCallback)(async () => {
291
+ setBusy(true);
292
+ setError(null);
293
+ try {
294
+ const resolved = resolveKeys(keys);
295
+ const res = await client.getStrings({
296
+ keys: resolved.length ? resolved : void 0
297
+ });
298
+ setStrings(res.strings);
299
+ } catch (e) {
300
+ setError(e instanceof Error ? e.message : "Failed to load strings");
301
+ } finally {
302
+ setBusy(false);
303
+ }
304
+ }, [client, keys]);
305
+ (0, import_react.useEffect)(() => {
306
+ if (consented) void loadStrings();
307
+ }, [consented, loadStrings]);
308
+ const accept = (0, import_react.useCallback)(async () => {
309
+ setBusy(true);
310
+ setError(null);
311
+ try {
312
+ await client.acceptTos();
313
+ setConsented(true);
314
+ } catch (e) {
315
+ setError(e instanceof Error ? e.message : "Could not accept the terms");
316
+ } finally {
317
+ setBusy(false);
318
+ }
319
+ }, [client]);
320
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
321
+ "div",
322
+ {
323
+ role: "dialog",
324
+ "aria-label": "Translation feedback",
325
+ style: {
326
+ position: "fixed",
327
+ inset: 0,
328
+ zIndex: 2147483600,
329
+ background: "rgba(0,0,0,.55)",
330
+ display: "flex",
331
+ justifyContent: "flex-end"
332
+ },
333
+ onClick: onClose,
334
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
335
+ "div",
336
+ {
337
+ onClick: (e) => e.stopPropagation(),
338
+ style: {
339
+ width: "min(420px, 100%)",
340
+ height: "100%",
341
+ background: C.bg,
342
+ color: C.text,
343
+ borderLeft: `1px solid ${C.border}`,
344
+ display: "flex",
345
+ flexDirection: "column",
346
+ fontFamily: "system-ui, sans-serif"
347
+ },
348
+ children: [
349
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
350
+ "header",
351
+ {
352
+ style: {
353
+ padding: "16px 18px",
354
+ borderBottom: `1px solid ${C.border}`,
355
+ display: "flex",
356
+ justifyContent: "space-between",
357
+ alignItems: "center"
358
+ },
359
+ children: [
360
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { style: { color: C.emeraldSoft }, children: "Translation feedback" }),
361
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
362
+ "button",
363
+ {
364
+ type: "button",
365
+ onClick: onClose,
366
+ "aria-label": "Close",
367
+ style: {
368
+ background: "transparent",
369
+ color: C.dim,
370
+ border: "none",
371
+ fontSize: 20,
372
+ cursor: "pointer"
373
+ },
374
+ children: "\xD7"
375
+ }
376
+ )
377
+ ]
378
+ }
379
+ ),
380
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { padding: 18, overflowY: "auto", flex: 1 }, children: [
381
+ error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { color: "#f87171", fontSize: 13 }, children: error }),
382
+ !consented ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
383
+ ConsentStep,
384
+ {
385
+ version: client.tosVersion,
386
+ busy,
387
+ onAccept: accept
388
+ }
389
+ ) : busy && !strings.length ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { color: C.dim }, children: "Loading\u2026" }) : !strings.length ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { color: C.dim }, children: "No strings to review on this view." }) : strings.map((s) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StringRow, { s, client }, `${s.namespace}:${s.key}`))
390
+ ] }),
391
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
392
+ "footer",
393
+ {
394
+ style: {
395
+ padding: "10px 18px",
396
+ borderTop: `1px solid ${C.border}`,
397
+ color: C.dim,
398
+ fontSize: 11
399
+ },
400
+ children: "Powered by Verbumia"
401
+ }
402
+ )
403
+ ]
404
+ }
405
+ )
406
+ }
407
+ );
408
+ }
409
+ function ConsentStep(props) {
410
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
411
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { lineHeight: 1.5, fontSize: 14 }, children: "Help improve the translations in this app. Your ratings and suggestions are sent to the app owner via Verbumia. Don\u2019t submit personal or sensitive information." }),
412
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
413
+ "button",
414
+ {
415
+ type: "button",
416
+ disabled: props.busy,
417
+ onClick: props.onAccept,
418
+ style: {
419
+ marginTop: 12,
420
+ width: "100%",
421
+ background: C.emerald,
422
+ color: "#03110c",
423
+ border: "none",
424
+ borderRadius: 8,
425
+ padding: "12px 14px",
426
+ fontWeight: 700,
427
+ cursor: props.busy ? "default" : "pointer",
428
+ opacity: props.busy ? 0.6 : 1
429
+ },
430
+ children: props.busy ? "\u2026" : `I accept the terms${props.version ? ` (v${props.version})` : ""}`
431
+ }
432
+ )
433
+ ] });
434
+ }
435
+ function StringRow(props) {
436
+ const { s, client } = props;
437
+ const [mine, setMine] = (0, import_react.useState)(s.my_rating);
438
+ const [showSuggest, setShowSuggest] = (0, import_react.useState)(false);
439
+ const [text, setText] = (0, import_react.useState)("");
440
+ const [sent, setSent] = (0, import_react.useState)(false);
441
+ const rate = (stars) => {
442
+ setMine(stars);
443
+ client.rate({
444
+ namespace: s.namespace,
445
+ key: s.key,
446
+ language: client.language,
447
+ translation_hash: s.translation_hash,
448
+ stars
449
+ });
450
+ };
451
+ const submitSuggestion = () => {
452
+ if (!text.trim()) return;
453
+ client.suggest({
454
+ namespace: s.namespace,
455
+ key: s.key,
456
+ language: client.language,
457
+ translation_hash: s.translation_hash,
458
+ suggested_text: text.trim()
459
+ });
460
+ setSent(true);
461
+ setShowSuggest(false);
462
+ setText("");
463
+ };
464
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
465
+ "div",
466
+ {
467
+ style: {
468
+ background: C.panel,
469
+ border: `1px solid ${C.border}`,
470
+ borderRadius: 10,
471
+ padding: 12,
472
+ marginBottom: 10
473
+ },
474
+ children: [
475
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { fontSize: 12, color: C.dim }, children: [
476
+ s.namespace,
477
+ " \xB7 ",
478
+ s.key
479
+ ] }),
480
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { margin: "6px 0 10px", fontSize: 15 }, children: s.value }),
481
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 4 }, children: [
482
+ [1, 2, 3, 4, 5].map((n) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
483
+ "button",
484
+ {
485
+ type: "button",
486
+ "aria-label": `${n} star${n > 1 ? "s" : ""}`,
487
+ onClick: () => rate(n),
488
+ style: {
489
+ background: "transparent",
490
+ border: "none",
491
+ cursor: "pointer",
492
+ fontSize: 20,
493
+ color: mine && n <= mine ? C.emeraldSoft : C.border
494
+ },
495
+ children: "\u2605"
496
+ },
497
+ n
498
+ )),
499
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
500
+ "button",
501
+ {
502
+ type: "button",
503
+ onClick: () => setShowSuggest((v) => !v),
504
+ style: {
505
+ marginLeft: "auto",
506
+ background: "transparent",
507
+ color: C.emeraldSoft,
508
+ border: `1px solid ${C.border}`,
509
+ borderRadius: 6,
510
+ padding: "4px 10px",
511
+ fontSize: 12,
512
+ cursor: "pointer"
513
+ },
514
+ children: sent ? "Suggested \u2713" : "Suggest"
515
+ }
516
+ )
517
+ ] }),
518
+ showSuggest && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { marginTop: 10 }, children: [
519
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
520
+ "textarea",
521
+ {
522
+ value: text,
523
+ onChange: (e) => setText(e.target.value),
524
+ rows: 3,
525
+ placeholder: "Your suggested translation\u2026",
526
+ style: {
527
+ width: "100%",
528
+ background: C.bg,
529
+ color: C.text,
530
+ border: `1px solid ${C.border}`,
531
+ borderRadius: 6,
532
+ padding: 8,
533
+ fontSize: 14,
534
+ resize: "vertical"
535
+ }
536
+ }
537
+ ),
538
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
539
+ "button",
540
+ {
541
+ type: "button",
542
+ onClick: submitSuggestion,
543
+ style: {
544
+ marginTop: 6,
545
+ background: C.emerald,
546
+ color: "#03110c",
547
+ border: "none",
548
+ borderRadius: 6,
549
+ padding: "8px 12px",
550
+ fontWeight: 700,
551
+ cursor: "pointer"
552
+ },
553
+ children: "Send suggestion"
554
+ }
555
+ )
556
+ ] })
557
+ ]
558
+ }
559
+ );
560
+ }
561
+
562
+ // src/react/plugin.tsx
563
+ var import_jsx_runtime2 = require("react/jsx-runtime");
564
+ function makeStore() {
565
+ let open = false;
566
+ const listeners = /* @__PURE__ */ new Set();
567
+ return {
568
+ isOpen: () => open,
569
+ set(v) {
570
+ if (open !== v) {
571
+ open = v;
572
+ listeners.forEach((l) => l());
573
+ }
574
+ },
575
+ subscribe(l) {
576
+ listeners.add(l);
577
+ return () => listeners.delete(l);
578
+ }
579
+ };
580
+ }
581
+ function feedbackPlugin(options) {
582
+ const store = makeStore();
583
+ let client = null;
584
+ function Outlet() {
585
+ const isOpen = (0, import_react2.useSyncExternalStore)(
586
+ store.subscribe,
587
+ store.isOpen,
588
+ store.isOpen
589
+ );
590
+ if (!isOpen || !client || typeof document === "undefined") return null;
591
+ const c = client;
592
+ return (0, import_react_dom.createPortal)(
593
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
594
+ FeedbackPanel,
595
+ {
596
+ client: c,
597
+ keys: options.keys,
598
+ onClose: () => {
599
+ store.set(false);
600
+ void c.flush();
601
+ }
602
+ }
603
+ ),
604
+ document.body
605
+ );
606
+ }
607
+ return {
608
+ name: "@verbumia/feedback",
609
+ setup(ctx) {
610
+ client = new FeedbackClient({
611
+ apiBase: options.apiBase ?? ctx.config.apiBase ?? "https://api.verbumia.ca",
612
+ projectId: options.projectId ?? ctx.config.projectUuid,
613
+ language: options.language ?? ctx.config.defaultLocale,
614
+ endUserId: options.endUserId,
615
+ fetchImpl: options.fetchImpl
616
+ });
617
+ const controller = {
618
+ open: () => store.set(true),
619
+ close: () => {
620
+ store.set(false);
621
+ void client?.flush();
622
+ },
623
+ client
624
+ };
625
+ options.onReady?.(controller);
626
+ if (options.controllerRef) options.controllerRef.current = controller;
627
+ return () => {
628
+ if (options.controllerRef) options.controllerRef.current = null;
629
+ void client?.flush();
630
+ };
631
+ },
632
+ render: () => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Outlet, {})
633
+ };
634
+ }
635
+ // Annotate the CommonJS export names for ESM import in node:
636
+ 0 && (module.exports = {
637
+ FeedbackClient,
638
+ FeedbackError,
639
+ FeedbackPanel,
640
+ feedbackPlugin,
641
+ hasKeyRegistry,
642
+ resolveKeys
643
+ });
644
+ //# sourceMappingURL=index.cjs.map