@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.
- package/CONTRACT.md +165 -0
- package/LICENSE +21 -0
- package/README.md +80 -0
- package/dist/chunk-5NA2TFPG.js +1 -0
- package/dist/chunk-5NA2TFPG.js.map +1 -0
- package/dist/chunk-OX4RJD5H.js +242 -0
- package/dist/chunk-OX4RJD5H.js.map +1 -0
- package/dist/client-CPEcvn23.d.cts +159 -0
- package/dist/client-CPEcvn23.d.ts +159 -0
- package/dist/core/index.cjs +272 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +18 -0
- package/dist/core/index.d.ts +18 -0
- package/dist/core/index.js +16 -0
- package/dist/core/index.js.map +1 -0
- package/dist/keys-BySe1O6V.d.ts +25 -0
- package/dist/keys-Dg_nv16u.d.cts +25 -0
- package/dist/native/index.cjs +575 -0
- package/dist/native/index.cjs.map +1 -0
- package/dist/native/index.d.cts +54 -0
- package/dist/native/index.d.ts +54 -0
- package/dist/native/index.js +322 -0
- package/dist/native/index.js.map +1 -0
- package/dist/react/index.cjs +644 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +55 -0
- package/dist/react/index.d.ts +55 -0
- package/dist/react/index.js +384 -0
- package/dist/react/index.js.map +1 -0
- package/dist/svelte/index.cjs +306 -0
- package/dist/svelte/index.cjs.map +1 -0
- package/dist/svelte/index.d.cts +38 -0
- package/dist/svelte/index.d.ts +38 -0
- package/dist/svelte/index.js +52 -0
- package/dist/svelte/index.js.map +1 -0
- package/dist/vue/index.cjs +426 -0
- package/dist/vue/index.cjs.map +1 -0
- package/dist/vue/index.d.cts +39 -0
- package/dist/vue/index.d.ts +39 -0
- package/dist/vue/index.js +172 -0
- package/dist/vue/index.js.map +1 -0
- 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
|