@verbumia/feedback 0.2.6 → 0.2.8

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 (37) hide show
  1. package/dist/chunk-EKU6NNWI.js +40 -0
  2. package/dist/chunk-EKU6NNWI.js.map +1 -0
  3. package/dist/{chunk-5RTFWOGT.js → chunk-EMGN6MR4.js} +87 -3
  4. package/dist/chunk-EMGN6MR4.js.map +1 -0
  5. package/dist/{client-qgDSbz3A.d.cts → client-CnEK_2SD.d.cts} +114 -1
  6. package/dist/{client-qgDSbz3A.d.ts → client-CnEK_2SD.d.ts} +114 -1
  7. package/dist/core/index.cjs +86 -2
  8. package/dist/core/index.cjs.map +1 -1
  9. package/dist/core/index.d.cts +2 -2
  10. package/dist/core/index.d.ts +2 -2
  11. package/dist/core/index.js +1 -1
  12. package/dist/{keys-BhuK_fy1.d.cts → keys-2_T5bDpX.d.cts} +1 -1
  13. package/dist/{keys-CEWu0Htb.d.ts → keys-eHc_lx5v.d.ts} +1 -1
  14. package/dist/native/index.cjs +317 -48
  15. package/dist/native/index.cjs.map +1 -1
  16. package/dist/native/index.d.cts +48 -4
  17. package/dist/native/index.d.ts +48 -4
  18. package/dist/native/index.js +200 -48
  19. package/dist/native/index.js.map +1 -1
  20. package/dist/react/index.cjs +319 -41
  21. package/dist/react/index.cjs.map +1 -1
  22. package/dist/react/index.d.cts +119 -4
  23. package/dist/react/index.d.ts +119 -4
  24. package/dist/react/index.js +202 -41
  25. package/dist/react/index.js.map +1 -1
  26. package/dist/svelte/index.cjs +86 -2
  27. package/dist/svelte/index.cjs.map +1 -1
  28. package/dist/svelte/index.d.cts +2 -2
  29. package/dist/svelte/index.d.ts +2 -2
  30. package/dist/svelte/index.js +1 -1
  31. package/dist/vue/index.cjs +86 -2
  32. package/dist/vue/index.cjs.map +1 -1
  33. package/dist/vue/index.d.cts +2 -2
  34. package/dist/vue/index.d.ts +2 -2
  35. package/dist/vue/index.js +1 -1
  36. package/package.json +1 -1
  37. package/dist/chunk-5RTFWOGT.js.map +0 -1
@@ -1,10 +1,13 @@
1
+ import {
2
+ t
3
+ } from "../chunk-EKU6NNWI.js";
1
4
  import "../chunk-5NA2TFPG.js";
2
5
  import {
3
6
  FeedbackClient,
4
7
  FeedbackError,
5
8
  hasKeyRegistry,
6
9
  resolveKeys
7
- } from "../chunk-5RTFWOGT.js";
10
+ } from "../chunk-EMGN6MR4.js";
8
11
 
9
12
  // src/react/plugin.tsx
10
13
  import { createPortal } from "react-dom";
@@ -12,7 +15,7 @@ import { useSyncExternalStore } from "react";
12
15
 
13
16
  // src/react/panel.tsx
14
17
  import { useCallback, useEffect, useState } from "react";
15
- import { jsx, jsxs } from "react/jsx-runtime";
18
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
16
19
  var C = {
17
20
  bg: "#0b0f0e",
18
21
  panel: "#111714",
@@ -23,11 +26,14 @@ var C = {
23
26
  emeraldSoft: "#34d399"
24
27
  };
25
28
  function FeedbackPanel(props) {
26
- const { client, keys, namespace, onClose } = props;
27
- const [consented, setConsented] = useState(client.hasConsented);
29
+ const { client, keys, namespace, tos = "modal", onClose } = props;
30
+ const [consented, setConsented] = useState(
31
+ tos === "skip" ? true : client.hasConsented
32
+ );
28
33
  const [busy, setBusy] = useState(false);
29
34
  const [error, setError] = useState(null);
30
35
  const [strings, setStrings] = useState([]);
36
+ const [showSource, setShowSource] = useState(false);
31
37
  const loadStrings = useCallback(async () => {
32
38
  setBusy(true);
33
39
  setError(null);
@@ -134,7 +140,52 @@ function FeedbackPanel(props) {
134
140
  busy,
135
141
  onAccept: accept
136
142
  }
137
- ) : busy && !strings.length ? /* @__PURE__ */ jsx("p", { style: { color: C.dim }, children: "Loading\u2026" }) : !strings.length ? /* @__PURE__ */ jsx("p", { style: { color: C.dim }, children: "No strings to review on this view." }) : strings.map((s) => /* @__PURE__ */ jsx(StringRow, { s, client }, `${s.namespace}:${s.key}`))
143
+ ) : busy && !strings.length ? /* @__PURE__ */ jsx("p", { style: { color: C.dim }, children: "Loading\u2026" }) : !strings.length ? /* @__PURE__ */ jsx("p", { style: { color: C.dim }, children: "No strings to review on this view." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
144
+ /* @__PURE__ */ jsx(
145
+ "div",
146
+ {
147
+ style: {
148
+ display: "flex",
149
+ alignItems: "center",
150
+ justifyContent: "flex-end",
151
+ marginBottom: 10
152
+ },
153
+ children: /* @__PURE__ */ jsxs(
154
+ "label",
155
+ {
156
+ style: {
157
+ display: "inline-flex",
158
+ alignItems: "center",
159
+ gap: 8,
160
+ color: C.dim,
161
+ fontSize: 12,
162
+ cursor: "pointer"
163
+ },
164
+ children: [
165
+ /* @__PURE__ */ jsx(
166
+ "input",
167
+ {
168
+ type: "checkbox",
169
+ checked: showSource,
170
+ onChange: (e) => setShowSource(e.target.checked)
171
+ }
172
+ ),
173
+ t(client.language, "showOriginal")
174
+ ]
175
+ }
176
+ )
177
+ }
178
+ ),
179
+ strings.map((s) => /* @__PURE__ */ jsx(
180
+ StringRow,
181
+ {
182
+ s,
183
+ client,
184
+ showSource
185
+ },
186
+ `${s.namespace}:${s.key}`
187
+ ))
188
+ ] })
138
189
  ] }),
139
190
  /* @__PURE__ */ jsx(
140
191
  "footer",
@@ -181,11 +232,13 @@ function ConsentStep(props) {
181
232
  ] });
182
233
  }
183
234
  function StringRow(props) {
184
- const { s, client } = props;
235
+ const { s, client, showSource } = props;
185
236
  const [mine, setMine] = useState(s.my_rating);
186
237
  const [showSuggest, setShowSuggest] = useState(false);
187
- const [text, setText] = useState("");
238
+ const mySuggestionId = s.my_suggestion ? s.my_suggestion.id : null;
239
+ const [text, setText] = useState(s.my_suggestion?.text ?? "");
188
240
  const [sent, setSent] = useState(false);
241
+ const [editError, setEditError] = useState(null);
189
242
  const rate = (stars) => {
190
243
  setMine(stars);
191
244
  client.rate({
@@ -196,19 +249,35 @@ function StringRow(props) {
196
249
  stars
197
250
  });
198
251
  };
199
- const submitSuggestion = () => {
200
- if (!text.trim()) return;
201
- client.suggest({
202
- namespace: s.namespace,
203
- key: s.key,
204
- language: client.language,
205
- translation_hash: s.translation_hash,
206
- suggested_text: text.trim()
207
- });
208
- setSent(true);
209
- setShowSuggest(false);
210
- setText("");
252
+ const submitSuggestion = async () => {
253
+ const trimmed = text.trim();
254
+ if (!trimmed) return;
255
+ setEditError(null);
256
+ try {
257
+ if (mySuggestionId) {
258
+ await client.editSuggestion(mySuggestionId, trimmed);
259
+ } else {
260
+ client.suggest({
261
+ namespace: s.namespace,
262
+ key: s.key,
263
+ language: client.language,
264
+ translation_hash: s.translation_hash,
265
+ suggested_text: trimmed
266
+ });
267
+ }
268
+ setSent(true);
269
+ setShowSuggest(false);
270
+ } catch (e) {
271
+ setEditError(
272
+ e instanceof Error ? e.message : "Could not update the suggestion"
273
+ );
274
+ }
211
275
  };
276
+ const showSourceRow = showSource && s.source_locale !== client.language;
277
+ const submitLabel = t(
278
+ client.language,
279
+ mySuggestionId ? "updateMySuggestion" : "submitSuggestion"
280
+ );
212
281
  return /* @__PURE__ */ jsxs(
213
282
  "div",
214
283
  {
@@ -225,6 +294,19 @@ function StringRow(props) {
225
294
  " \xB7 ",
226
295
  s.key
227
296
  ] }),
297
+ showSourceRow ? /* @__PURE__ */ jsx(
298
+ "div",
299
+ {
300
+ "data-testid": "source-row",
301
+ style: {
302
+ margin: "4px 0 2px",
303
+ fontSize: 12,
304
+ color: C.dim,
305
+ fontStyle: "italic"
306
+ },
307
+ children: s.source_text
308
+ }
309
+ ) : null,
228
310
  /* @__PURE__ */ jsx("div", { style: { margin: "6px 0 10px", fontSize: 15 }, children: s.value }),
229
311
  /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 4 }, children: [
230
312
  [1, 2, 3, 4, 5].map((n) => /* @__PURE__ */ jsx(
@@ -259,7 +341,7 @@ function StringRow(props) {
259
341
  fontSize: 12,
260
342
  cursor: "pointer"
261
343
  },
262
- children: sent ? "Suggested \u2713" : "Suggest"
344
+ children: sent ? "Suggested \u2713" : submitLabel
263
345
  }
264
346
  )
265
347
  ] }),
@@ -283,11 +365,12 @@ function StringRow(props) {
283
365
  }
284
366
  }
285
367
  ),
368
+ editError ? /* @__PURE__ */ jsx("p", { style: { color: "#f87171", fontSize: 12, marginTop: 6 }, children: editError }) : null,
286
369
  /* @__PURE__ */ jsx(
287
370
  "button",
288
371
  {
289
372
  type: "button",
290
- onClick: submitSuggestion,
373
+ onClick: () => void submitSuggestion(),
291
374
  style: {
292
375
  marginTop: 6,
293
376
  background: C.emerald,
@@ -298,7 +381,7 @@ function StringRow(props) {
298
381
  fontWeight: 700,
299
382
  cursor: "pointer"
300
383
  },
301
- children: "Send suggestion"
384
+ children: submitLabel
302
385
  }
303
386
  )
304
387
  ] })
@@ -310,42 +393,73 @@ function StringRow(props) {
310
393
  // src/react/plugin.tsx
311
394
  import { jsx as jsx2 } from "react/jsx-runtime";
312
395
  function makeStore() {
313
- let open = false;
396
+ let state = { isOpen: false, snapshotKeys: void 0 };
314
397
  const listeners = /* @__PURE__ */ new Set();
315
398
  return {
316
- isOpen: () => open,
317
- set(v) {
318
- if (open !== v) {
319
- open = v;
320
- listeners.forEach((l) => l());
399
+ getState: () => state,
400
+ setOpen(open, snapshotKeys) {
401
+ const next = {
402
+ isOpen: open,
403
+ snapshotKeys: open ? snapshotKeys : void 0
404
+ };
405
+ if (state.isOpen === next.isOpen && state.snapshotKeys === next.snapshotKeys) {
406
+ return;
321
407
  }
408
+ state = next;
409
+ listeners.forEach((l) => l());
322
410
  },
323
411
  subscribe(l) {
324
412
  listeners.add(l);
325
- return () => listeners.delete(l);
413
+ return () => {
414
+ listeners.delete(l);
415
+ };
326
416
  }
327
417
  };
328
418
  }
419
+ async function captureCurrentViewSnapshot(i18next) {
420
+ const reg = globalThis.__verbumia_key_registry__;
421
+ if (!reg) return [];
422
+ const lng = i18next?.language;
423
+ const change = i18next?.changeLanguage;
424
+ if (typeof change === "function" && typeof lng === "string" && lng) {
425
+ reg.reset?.();
426
+ try {
427
+ await change(lng);
428
+ } catch {
429
+ }
430
+ await new Promise((r) => {
431
+ if (typeof requestAnimationFrame === "function") {
432
+ requestAnimationFrame(() => r());
433
+ } else {
434
+ setTimeout(() => r(), 16);
435
+ }
436
+ });
437
+ return reg.snapshot();
438
+ }
439
+ return reg.snapshot();
440
+ }
329
441
  function feedbackPlugin(options) {
330
442
  const store = makeStore();
331
443
  let client = null;
332
444
  function Outlet() {
333
- const isOpen = useSyncExternalStore(
445
+ const state = useSyncExternalStore(
334
446
  store.subscribe,
335
- store.isOpen,
336
- store.isOpen
447
+ store.getState,
448
+ store.getState
337
449
  );
338
- if (!isOpen || !client || typeof document === "undefined") return null;
450
+ if (!state.isOpen || !client || typeof document === "undefined") return null;
339
451
  const c = client;
452
+ const panelKeys = options.keys ?? state.snapshotKeys;
340
453
  return createPortal(
341
454
  /* @__PURE__ */ jsx2(
342
455
  FeedbackPanel,
343
456
  {
344
457
  client: c,
345
- keys: options.keys,
458
+ keys: panelKeys,
346
459
  namespace: options.namespace,
460
+ tos: options.tos ?? "modal",
347
461
  onClose: () => {
348
- store.set(false);
462
+ store.setOpen(false);
349
463
  void c.flush();
350
464
  }
351
465
  }
@@ -357,24 +471,71 @@ function feedbackPlugin(options) {
357
471
  name: "@verbumia/feedback",
358
472
  setup(ctx) {
359
473
  const initialLanguage = options.language ?? ctx.i18n?.language ?? ctx.config.defaultLocale;
474
+ const tos = options.tos ?? "modal";
360
475
  client = new FeedbackClient({
361
476
  apiBase: options.apiBase ?? ctx.config.apiBase ?? "https://api.verbumia.dev",
362
477
  projectId: options.projectId ?? ctx.config.projectUuid,
363
478
  language: initialLanguage,
364
479
  endUserId: options.endUserId,
365
- fetchImpl: options.fetchImpl
480
+ fetchImpl: options.fetchImpl,
481
+ apiKey: options.apiKey,
482
+ autoAcceptTos: tos !== "skip"
366
483
  });
484
+ const clientRef = client;
485
+ let addonState = null;
486
+ const cta = options.cta ?? "auto";
487
+ const scope = options.scope ?? "current-view";
488
+ const ctxI18next = ctx.i18n?.i18next;
489
+ const refreshState = async () => {
490
+ try {
491
+ const next = await clientRef.getAddonState();
492
+ if (next !== null) addonState = next;
493
+ } catch {
494
+ }
495
+ };
496
+ if (options.apiKey) void refreshState();
367
497
  let langUnsub;
368
498
  if (typeof ctx.onLanguageChange === "function") {
369
- langUnsub = ctx.onLanguageChange((lng) => client?.setLanguage(lng));
499
+ langUnsub = ctx.onLanguageChange((lng) => {
500
+ clientRef.setLanguage(lng);
501
+ void refreshState();
502
+ });
370
503
  }
371
504
  const controller = {
372
- open: () => store.set(true),
505
+ open: async () => {
506
+ if (scope === "current-view") {
507
+ const snapshot = await captureCurrentViewSnapshot(ctxI18next);
508
+ store.setOpen(true, snapshot);
509
+ return;
510
+ }
511
+ store.setOpen(true);
512
+ },
373
513
  close: () => {
374
- store.set(false);
375
- void client?.flush();
514
+ store.setOpen(false);
515
+ void clientRef?.flush();
516
+ },
517
+ client: clientRef,
518
+ get tosVersion() {
519
+ return clientRef.tosVersion;
520
+ },
521
+ get hasAcceptedTos() {
522
+ return clientRef.hasAcceptedTos;
376
523
  },
377
- client
524
+ acceptTos: async () => {
525
+ await clientRef.acceptTos();
526
+ if (!options.apiKey) await refreshState();
527
+ },
528
+ get isActive() {
529
+ if (cta === "show") return true;
530
+ if (cta === "hide") return false;
531
+ return addonState ? addonState.isActive : null;
532
+ },
533
+ get enabledLanguages() {
534
+ return addonState ? addonState.enabledLanguages : null;
535
+ },
536
+ get sku() {
537
+ return addonState ? addonState.sku : null;
538
+ }
378
539
  };
379
540
  options.onReady?.(controller);
380
541
  if (options.controllerRef) options.controllerRef.current = controller;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/react/plugin.tsx","../../src/react/panel.tsx"],"sourcesContent":["/**\n * `@verbumia/feedback` as a PLUGIN of the `@verbumia/*-i18n` provider.\n *\n * Architecture (task 599): NO second React context. The customer adds\n * `feedbackPlugin(...)` to the i18n provider's `plugins` slot. The\n * provider calls `setup({ i18n, config })` once (we reuse its apiBase /\n * projectUuid / locale — no re-config) and renders our `render()` as an\n * ISOLATED sibling leaf. The panel's open/close lives in a tiny private\n * store the outlet subscribes to via useSyncExternalStore, so toggling\n * feedback NEVER re-renders the host app. The customer triggers it via\n * their own CTA through the imperative controller.\n */\nimport { createPortal } from \"react-dom\";\nimport { useSyncExternalStore, type ReactNode } from \"react\";\n\nimport { FeedbackClient } from \"../core/client\";\nimport type { DeclaredKey } from \"../core/types\";\nimport { FeedbackPanel } from \"./panel\";\n\n/** Structural mirror of `@verbumia/react-i18next`'s VerbumiaPlugin —\n * kept local so feedback has no build-time dep on the i18n SDK. The\n * `i18n` and `onLanguageChange` fields are optional so a plugin attached\n * to a non-Verbumia host (or to a pre-1.0.5 react-i18next) keeps\n * working — runtime language re-sync is just disabled. From feedback\n * ≥0.2.6, the peerDep on `@verbumia/react-i18next` is `>=1.0.5`, so the\n * lang-change subscription path is guaranteed available in matched\n * installs. */\nexport interface I18nPluginContext {\n i18n?: { language?: string };\n config: {\n apiBase?: string;\n projectUuid: string;\n defaultLocale: string;\n };\n /** #806 — subscribe to runtime language changes (see VerbumiaPluginContext\n * in `@verbumia/react-i18next` ≥1.0.5). Returns an unsubscribe fn the\n * plugin MUST call from teardown. Optional so attaching to a\n * pre-1.0.5 react-i18next falls back gracefully (no auto re-sync;\n * consumer can still pass `options.language` explicitly). */\n onLanguageChange?: (cb: (lng: string) => void) => () => void;\n}\nexport interface I18nPlugin {\n name: string;\n setup?: (ctx: I18nPluginContext) => void | (() => void);\n render?: () => ReactNode;\n}\n\nexport interface FeedbackController {\n open: () => void;\n close: () => void;\n /** The underlying client (advanced; usually unused). */\n client: FeedbackClient;\n}\n\nexport interface FeedbackPluginOptions {\n // NOTE: no `tosVersion` — it is an SDK build-time constant\n // (SDK_TOS_VERSION, task 616), sent automatically; never integrator-set.\n /** Override language; defaults to the i18n provider's defaultLocale. */\n language?: string;\n /** Override apiBase; defaults to the i18n provider's apiBase. */\n apiBase?: string;\n /** Override projectId; defaults to the i18n provider's projectUuid. */\n projectId?: string;\n /** Optional opaque end-user id (server generates one when absent). */\n endUserId?: string;\n /** Explicit on-screen keys (else the i18n key registry, if present). */\n keys?: DeclaredKey[];\n /** Optional namespace filter (CONTRACT v6, additive). When set (a\n * single namespace or a list) the panel shows only resolved keys in\n * that namespace — applied AFTER rendered-scoping (`rendered ∩\n * namespace`). Unset ⇒ all resolved keys (v5 behaviour). */\n namespace?: string | string[];\n /** Receives the imperative { open, close } handle for the host CTA. */\n onReady?: (controller: FeedbackController) => void;\n /** Alt delivery: a ref object that receives the controller. */\n controllerRef?: { current: FeedbackController | null };\n /** Injected fetch (tests / RN polyfills). */\n fetchImpl?: typeof fetch;\n}\n\nfunction makeStore() {\n let open = false;\n const listeners = new Set<() => void>();\n return {\n isOpen: () => open,\n set(v: boolean) {\n if (open !== v) {\n open = v;\n listeners.forEach((l) => l());\n }\n },\n subscribe(l: () => void) {\n listeners.add(l);\n return () => listeners.delete(l);\n },\n };\n}\n\nexport function feedbackPlugin(options: FeedbackPluginOptions): I18nPlugin {\n const store = makeStore();\n let client: FeedbackClient | null = null;\n\n function Outlet() {\n const isOpen = useSyncExternalStore(\n store.subscribe,\n store.isOpen,\n store.isOpen,\n );\n if (!isOpen || !client || typeof document === \"undefined\") return null;\n const c = client;\n return createPortal(\n <FeedbackPanel\n client={c}\n keys={options.keys}\n namespace={options.namespace}\n onClose={() => {\n store.set(false);\n void c.flush();\n }}\n />,\n document.body,\n );\n }\n\n return {\n name: \"@verbumia/feedback\",\n setup(ctx) {\n // #806 SeedSower lang-change init source: prefer an explicit\n // plugin option, then the i18n provider's CURRENT language (so\n // mounts that happen after a boot-time language flip get it\n // right), then the boot-snapshot defaultLocale as the floor.\n const initialLanguage =\n options.language ?? ctx.i18n?.language ?? ctx.config.defaultLocale;\n client = new FeedbackClient({\n apiBase:\n options.apiBase ?? ctx.config.apiBase ?? \"https://api.verbumia.dev\",\n projectId: options.projectId ?? ctx.config.projectUuid,\n language: initialLanguage,\n endUserId: options.endUserId,\n fetchImpl: options.fetchImpl,\n });\n // #806 — re-sync the client whenever the host changes the\n // language at runtime. Optional: if the host's i18n provider\n // predates the plugin-context `onLanguageChange` field (or the\n // plugin is mounted on a non-Verbumia host), this just no-ops\n // and the consumer can still pass `options.language` explicitly.\n let langUnsub: (() => void) | undefined;\n if (typeof ctx.onLanguageChange === \"function\") {\n langUnsub = ctx.onLanguageChange((lng) => client?.setLanguage(lng));\n }\n const controller: FeedbackController = {\n open: () => store.set(true),\n close: () => {\n store.set(false);\n void client?.flush();\n },\n client,\n };\n options.onReady?.(controller);\n if (options.controllerRef) options.controllerRef.current = controller;\n return () => {\n langUnsub?.();\n if (options.controllerRef) options.controllerRef.current = null;\n void client?.flush();\n };\n },\n render: () => <Outlet />,\n };\n}\n","import { useCallback, useEffect, useState } from \"react\";\n\nimport type { FeedbackClient } from \"../core/client\";\nimport { resolveKeys } from \"../core/keys\";\nimport type { DeclaredKey, FeedbackString } from \"../core/types\";\n\nconst C = {\n bg: \"#0b0f0e\",\n panel: \"#111714\",\n border: \"#1f2a25\",\n text: \"#e7f5ef\",\n dim: \"#8aa79b\",\n emerald: \"#10b981\",\n emeraldSoft: \"#34d399\",\n};\n\nexport function FeedbackPanel(props: {\n client: FeedbackClient;\n keys?: DeclaredKey[];\n namespace?: string | string[];\n onClose: () => void;\n}) {\n const { client, keys, namespace, onClose } = props;\n const [consented, setConsented] = useState(client.hasConsented);\n const [busy, setBusy] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [strings, setStrings] = useState<FeedbackString[]>([]);\n\n const loadStrings = useCallback(async () => {\n setBusy(true);\n setError(null);\n try {\n // On-screen scoping is NORMATIVE (spec ltm 373, CONTRACT v5): show\n // ONLY rendered keys. Empty -> \"no strings on this view\"; never\n // fall back to fetching the whole project.\n const resolved = resolveKeys(keys, namespace);\n if (!resolved.length) {\n // #806 SeedSower P1 — when the panel opens with NO explicit\n // `keys` prop (i.e. it depended on the i18n SDK's on-screen\n // registry), an empty resolve almost always means the host's\n // `useTranslation` imports went to `react-i18next` instead of\n // `@verbumia/react-i18next`. Either way, the keys never fed the\n // registry. Log a single, actionable hint so the integrator\n // doesn't waste a debug cycle on \"the widget is broken\".\n if (!keys || keys.length === 0) {\n // eslint-disable-next-line no-console\n console.warn(\n \"[verbumia/feedback] registry empty — are your useTranslation imports coming from @verbumia/react-i18next?\",\n );\n }\n setStrings([]);\n return;\n }\n const res = await client.getStrings({ keys: resolved });\n setStrings(res.strings);\n } catch (e) {\n setError(e instanceof Error ? e.message : \"Failed to load strings\");\n } finally {\n setBusy(false);\n }\n }, [client, keys, namespace]);\n\n useEffect(() => {\n if (consented) void loadStrings();\n }, [consented, loadStrings]);\n\n const accept = useCallback(async () => {\n setBusy(true);\n setError(null);\n try {\n await client.acceptTos();\n setConsented(true);\n } catch (e) {\n setError(e instanceof Error ? e.message : \"Could not accept the terms\");\n } finally {\n setBusy(false);\n }\n }, [client]);\n\n return (\n <div\n role=\"dialog\"\n aria-label=\"Translation feedback\"\n style={{\n position: \"fixed\",\n inset: 0,\n zIndex: 2147483600,\n background: \"rgba(0,0,0,.55)\",\n display: \"flex\",\n justifyContent: \"flex-end\",\n }}\n onClick={onClose}\n >\n <div\n onClick={(e) => e.stopPropagation()}\n style={{\n width: \"min(420px, 100%)\",\n height: \"100%\",\n background: C.bg,\n color: C.text,\n borderLeft: `1px solid ${C.border}`,\n display: \"flex\",\n flexDirection: \"column\",\n fontFamily: \"system-ui, sans-serif\",\n }}\n >\n <header\n style={{\n padding: \"16px 18px\",\n borderBottom: `1px solid ${C.border}`,\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n }}\n >\n <strong style={{ color: C.emeraldSoft }}>Translation feedback</strong>\n <button\n type=\"button\"\n onClick={onClose}\n aria-label=\"Close\"\n style={{\n background: \"transparent\",\n color: C.dim,\n border: \"none\",\n fontSize: 20,\n cursor: \"pointer\",\n }}\n >\n ×\n </button>\n </header>\n\n <div style={{ padding: 18, overflowY: \"auto\", flex: 1 }}>\n {error && (\n <p style={{ color: \"#f87171\", fontSize: 13 }}>{error}</p>\n )}\n\n {!consented ? (\n <ConsentStep\n version={client.tosVersion}\n busy={busy}\n onAccept={accept}\n />\n ) : busy && !strings.length ? (\n <p style={{ color: C.dim }}>Loading…</p>\n ) : !strings.length ? (\n <p style={{ color: C.dim }}>No strings to review on this view.</p>\n ) : (\n strings.map((s) => (\n <StringRow key={`${s.namespace}:${s.key}`} s={s} client={client} />\n ))\n )}\n </div>\n <footer\n style={{\n padding: \"10px 18px\",\n borderTop: `1px solid ${C.border}`,\n color: C.dim,\n fontSize: 11,\n }}\n >\n Powered by Verbumia\n </footer>\n </div>\n </div>\n );\n}\n\nfunction ConsentStep(props: {\n version?: string;\n busy: boolean;\n onAccept: () => void;\n}) {\n return (\n <div>\n <p style={{ lineHeight: 1.5, fontSize: 14 }}>\n Help improve the translations in this app. Your ratings and\n suggestions are sent to the app owner via Verbumia. Don’t submit\n personal or sensitive information.\n </p>\n <button\n type=\"button\"\n disabled={props.busy}\n onClick={props.onAccept}\n style={{\n marginTop: 12,\n width: \"100%\",\n background: C.emerald,\n color: \"#03110c\",\n border: \"none\",\n borderRadius: 8,\n padding: \"12px 14px\",\n fontWeight: 700,\n cursor: props.busy ? \"default\" : \"pointer\",\n opacity: props.busy ? 0.6 : 1,\n }}\n >\n {props.busy\n ? \"…\"\n : `I accept the terms${props.version ? ` (v${props.version})` : \"\"}`}\n </button>\n </div>\n );\n}\n\nfunction StringRow(props: { s: FeedbackString; client: FeedbackClient }) {\n const { s, client } = props;\n const [mine, setMine] = useState<number | null>(s.my_rating);\n const [showSuggest, setShowSuggest] = useState(false);\n const [text, setText] = useState(\"\");\n const [sent, setSent] = useState(false);\n\n const rate = (stars: number) => {\n setMine(stars);\n client.rate({\n namespace: s.namespace,\n key: s.key,\n language: client.language,\n translation_hash: s.translation_hash,\n stars,\n });\n };\n\n const submitSuggestion = () => {\n if (!text.trim()) return;\n client.suggest({\n namespace: s.namespace,\n key: s.key,\n language: client.language,\n translation_hash: s.translation_hash,\n suggested_text: text.trim(),\n });\n setSent(true);\n setShowSuggest(false);\n setText(\"\");\n };\n\n return (\n <div\n style={{\n background: C.panel,\n border: `1px solid ${C.border}`,\n borderRadius: 10,\n padding: 12,\n marginBottom: 10,\n }}\n >\n <div style={{ fontSize: 12, color: C.dim }}>\n {s.namespace} · {s.key}\n </div>\n <div style={{ margin: \"6px 0 10px\", fontSize: 15 }}>{s.value}</div>\n <div style={{ display: \"flex\", gap: 4 }}>\n {[1, 2, 3, 4, 5].map((n) => (\n <button\n key={n}\n type=\"button\"\n aria-label={`${n} star${n > 1 ? \"s\" : \"\"}`}\n onClick={() => rate(n)}\n style={{\n background: \"transparent\",\n border: \"none\",\n cursor: \"pointer\",\n fontSize: 20,\n color: mine && n <= mine ? C.emeraldSoft : C.border,\n }}\n >\n ★\n </button>\n ))}\n <button\n type=\"button\"\n onClick={() => setShowSuggest((v) => !v)}\n style={{\n marginLeft: \"auto\",\n background: \"transparent\",\n color: C.emeraldSoft,\n border: `1px solid ${C.border}`,\n borderRadius: 6,\n padding: \"4px 10px\",\n fontSize: 12,\n cursor: \"pointer\",\n }}\n >\n {sent ? \"Suggested ✓\" : \"Suggest\"}\n </button>\n </div>\n {showSuggest && (\n <div style={{ marginTop: 10 }}>\n <textarea\n value={text}\n onChange={(e) => setText(e.target.value)}\n rows={3}\n placeholder=\"Your suggested translation…\"\n style={{\n width: \"100%\",\n background: C.bg,\n color: C.text,\n border: `1px solid ${C.border}`,\n borderRadius: 6,\n padding: 8,\n fontSize: 14,\n resize: \"vertical\",\n }}\n />\n <button\n type=\"button\"\n onClick={submitSuggestion}\n style={{\n marginTop: 6,\n background: C.emerald,\n color: \"#03110c\",\n border: \"none\",\n borderRadius: 6,\n padding: \"8px 12px\",\n fontWeight: 700,\n cursor: \"pointer\",\n }}\n >\n Send suggestion\n </button>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["jsx"]}
1
+ {"version":3,"sources":["../../src/react/plugin.tsx","../../src/react/panel.tsx"],"sourcesContent":["/**\n * `@verbumia/feedback` as a PLUGIN of the `@verbumia/*-i18n` provider.\n *\n * Architecture (task 599): NO second React context. The customer adds\n * `feedbackPlugin(...)` to the i18n provider's `plugins` slot. The\n * provider calls `setup({ i18n, config })` once (we reuse its apiBase /\n * projectUuid / locale — no re-config) and renders our `render()` as an\n * ISOLATED sibling leaf. The panel's open/close lives in a tiny private\n * store the outlet subscribes to via useSyncExternalStore, so toggling\n * feedback NEVER re-renders the host app. The customer triggers it via\n * their own CTA through the imperative controller.\n */\nimport { createPortal } from \"react-dom\";\nimport { useSyncExternalStore, type ReactNode } from \"react\";\n\nimport { FeedbackClient } from \"../core/client\";\nimport type { DeclaredKey, FeedbackAddonState } from \"../core/types\";\nimport { FeedbackPanel } from \"./panel\";\n\n/** Structural mirror of `@verbumia/react-i18next`'s VerbumiaPlugin —\n * kept local so feedback has no build-time dep on the i18n SDK. The\n * `i18n` and `onLanguageChange` fields are optional so a plugin attached\n * to a non-Verbumia host (or to a pre-1.0.5 react-i18next) keeps\n * working — runtime language re-sync is just disabled. From feedback\n * ≥0.2.6, the peerDep on `@verbumia/react-i18next` is `>=1.0.5`, so the\n * lang-change subscription path is guaranteed available in matched\n * installs. */\nexport interface I18nPluginContext {\n i18n?: {\n language?: string;\n /** 0.2.7 — direct handle on the underlying `i18next` instance the\n * `@verbumia/react-i18next` provider exposes. Used by the\n * `scope: \"current-view\"` snapshot path: a same-locale\n * `changeLanguage(language)` re-emits `languageChanged` so every\n * bound consumer re-renders → `t()` calls repopulate the registry.\n * Optional so the plugin still works on non-Verbumia hosts (it\n * falls back to a no-rerender registry snapshot in that case). */\n i18next?: {\n language?: string;\n changeLanguage?: (lng: string) => Promise<unknown>;\n };\n };\n config: {\n apiBase?: string;\n projectUuid: string;\n defaultLocale: string;\n };\n /** #806 — subscribe to runtime language changes (see VerbumiaPluginContext\n * in `@verbumia/react-i18next` ≥1.0.5). Returns an unsubscribe fn the\n * plugin MUST call from teardown. Optional so attaching to a\n * pre-1.0.5 react-i18next falls back gracefully (no auto re-sync;\n * consumer can still pass `options.language` explicitly). */\n onLanguageChange?: (cb: (lng: string) => void) => () => void;\n}\nexport interface I18nPlugin {\n name: string;\n setup?: (ctx: I18nPluginContext) => void | (() => void);\n render?: () => ReactNode;\n}\n\nexport interface FeedbackController {\n /**\n * Opens the panel. From 0.2.7 the call is **async** (returns\n * `Promise<void>`) so the `scope: \"current-view\"` snapshot sequence\n * can run before the modal mounts (reset registry → force\n * languageChanged → 1-frame yield → snapshot). Hosts that do NOT\n * await still work — the modal mounts when the promise resolves.\n * Under `scope: \"all\"` (or when the i18next instance isn't reachable)\n * `open()` resolves on the next microtask with no extra work.\n */\n open: () => Promise<void>;\n close: () => void;\n /** The underlying client (advanced; usually unused). */\n client: FeedbackClient;\n /** ToS version currently required by the backend (SDK build-time\n * constant — see core/tos.ts). 0.2.7+. */\n readonly tosVersion: string;\n /** Whether this end-user has a live session-token bundle (i.e. has\n * accepted the current ToS version). Mirrors the same persisted state\n * the built-in modal writes to, so an external ToS page that calls\n * `acceptTos()` flips this to `true` immediately. 0.2.7+. */\n readonly hasAcceptedTos: boolean;\n /** Programmatically POST `/v1/feedback/tos` and persist the returned\n * token bundle into the SDK's shared store. Used by hosts that\n * build their own ToS page (`feedbackPlugin({ tos: \"skip\" })`). Thin\n * alias over `FeedbackClient.acceptTos()` — idempotent (a second\n * call returns the in-flight / existing bundle without re-POSTing).\n * 0.2.7+. */\n acceptTos: () => Promise<void>;\n /** 0.2.7 — feedback-addon active flag for this project, sourced from\n * `GET /v1/projects/{p}/feedback-addon/state` + composed with the\n * plugin's `cta` option:\n * - `cta: \"auto\"` (default): mirrors `state.isActive` (so a host\n * that hides its CTA on `!isActive` does the right thing for\n * Starter / no SKU).\n * - `cta: \"show\"`: always `true` (force the host's CTA).\n * - `cta: \"hide\"`: always `false`.\n * `null` before the first state fetch resolves (e.g. on a host that\n * didn't pass `apiKey` and hasn't accepted ToS yet). */\n readonly isActive: boolean | null;\n /** 0.2.7 — the languages the SDK is licensed to rate strings in.\n * Mirrors `state.enabledLanguages` verbatim: `string[]` for Starter\n * / no SKU, literal `\"all\"` for Unlimited. `null` before the first\n * state fetch resolves. */\n readonly enabledLanguages: string[] | \"all\" | null;\n /** 0.2.7 — raw SKU code (`feedback_starter` / `feedback_unlimited`\n * / `null`). Useful for downstream UI / billing dashboards. */\n readonly sku: \"feedback_starter\" | \"feedback_unlimited\" | null;\n}\n\nexport interface FeedbackPluginOptions {\n // NOTE: no `tosVersion` — it is an SDK build-time constant\n // (SDK_TOS_VERSION, task 616), sent automatically; never integrator-set.\n /** Override language; defaults to the i18n provider's defaultLocale. */\n language?: string;\n /** Override apiBase; defaults to the i18n provider's apiBase. */\n apiBase?: string;\n /** Override projectId; defaults to the i18n provider's projectUuid. */\n projectId?: string;\n /** Optional opaque end-user id (server generates one when absent). */\n endUserId?: string;\n /** Explicit on-screen keys (else the i18n key registry, if present). */\n keys?: DeclaredKey[];\n /** Optional namespace filter (CONTRACT v6, additive). When set (a\n * single namespace or a list) the panel shows only resolved keys in\n * that namespace — applied AFTER rendered-scoping (`rendered ∩\n * namespace`). Unset ⇒ all resolved keys (v5 behaviour). */\n namespace?: string | string[];\n /** Receives the imperative { open, close } handle for the host CTA. */\n onReady?: (controller: FeedbackController) => void;\n /** Alt delivery: a ref object that receives the controller. */\n controllerRef?: { current: FeedbackController | null };\n /** Injected fetch (tests / RN polyfills). */\n fetchImpl?: typeof fetch;\n /**\n * 0.2.7 — how the SDK handles the Terms of Service prompt:\n * - `\"modal\"` (DEFAULT, backwards-compatible): the built-in modal\n * renders the SDK's ToS step on first open; tapping Accept calls\n * `POST /v1/feedback/tos` and persists the token bundle.\n * - `\"skip\"`: the built-in modal SKIPS the ToS step entirely. The\n * host promises to handle consent externally (e.g. their own\n * branded ToS page) and must call `controller.acceptTos()` to\n * mint the session bundle. Until that runs, every `getStrings()`\n * surfaces the existing 401 error state (the SDK does NOT\n * silently auto-accept on the user's behalf).\n */\n tos?: \"modal\" | \"skip\";\n /**\n * 0.2.7 — host's project API key (`vrb_live_…`, scope `project:read`).\n * Lets the SDK call `GET /v1/projects/{p}/feedback-addon/state` at\n * `setup()` time + on language change, BEFORE the end-user accepts\n * ToS. When omitted, addon-state is deferred until the user's\n * session bearer is minted via `acceptTos()`, and `controller.isActive`\n * stays `null` until then.\n */\n apiKey?: string;\n /**\n * 0.2.7 — controls `controller.isActive` (which hosts wire to their\n * own CTA's `hidden` flag — the SDK does not render a floating CTA\n * itself; see plugin.tsx header).\n * - `\"auto\"` (DEFAULT): mirrors `state.isActive` from the backend,\n * i.e. hides on Starter / no SKU, shows on Unlimited.\n * - `\"show\"`: forces `isActive=true` regardless of state.\n * - `\"hide\"`: forces `isActive=false` regardless of state.\n */\n cta?: \"auto\" | \"show\" | \"hide\";\n /**\n * 0.2.7 — what the panel calls \"on-screen keys\":\n * - `\"current-view\"` (DEFAULT — rebalances the 1.0.3 \"strict-better-\n * than-false-empty\" trade-off now that we have a better mechanism):\n * `controller.open()` (now async) resets the on-screen key\n * registry, force-emits a same-locale `languageChanged` through\n * i18next to re-render every consumer, awaits a 1-frame yield so\n * their `t()` calls repopulate the registry, snapshots, then opens\n * the modal with that snapshot. Result: the widget lists ONLY keys\n * rendered on the user's current screen at the instant they\n * opened it.\n * - `\"all\"`: pre-0.2.7 behavior — no reset, no force re-render. The\n * panel reads the accumulated registry as-is (still strictly\n * better than a false-empty; useful if you want the cumulative\n * keys-since-mount view).\n *\n * KNOWN LIMITATION (`\"current-view\"`): components that call\n * `i18next.t()` OUTSIDE a React tree (cron-like calls, module-load\n * `t()`) are NOT re-collected by the changeLanguage trigger — they\n * have nothing to re-render. Acceptable: those calls aren't strictly\n * \"rendered on screen\", so excluding them matches the on-screen\n * contract.\n */\n scope?: \"current-view\" | \"all\";\n}\n\n/** 0.2.7 — panel store. `snapshotKeys` is the on-screen snapshot captured\n * by `controller.open()` when `scope: \"current-view\"`; the Outlet passes\n * it down as the panel's `keys` prop, where the precedence becomes\n * `options.keys` > snapshot > registry. `getSnapshot` must return a\n * stable reference between notifications (the useSyncExternalStore\n * contract), so the state object is reallocated only on real changes. */\ntype PanelState = {\n isOpen: boolean;\n snapshotKeys: DeclaredKey[] | undefined;\n};\nfunction makeStore() {\n let state: PanelState = { isOpen: false, snapshotKeys: undefined };\n const listeners = new Set<() => void>();\n return {\n getState: (): PanelState => state,\n setOpen(open: boolean, snapshotKeys?: DeclaredKey[]): void {\n const next: PanelState = {\n isOpen: open,\n snapshotKeys: open ? snapshotKeys : undefined,\n };\n if (\n state.isOpen === next.isOpen &&\n state.snapshotKeys === next.snapshotKeys\n ) {\n return;\n }\n state = next;\n listeners.forEach((l) => l());\n },\n subscribe(l: () => void): () => void {\n listeners.add(l);\n return () => {\n listeners.delete(l);\n };\n },\n };\n}\n\n/**\n * 0.2.7 — \"current-view\" snapshot capture. Resets the on-screen key\n * registry, forces every i18next-bound consumer to re-render via a\n * same-locale `changeLanguage` (which re-emits `languageChanged` without\n * triggering a CDN re-fetch — same locale value), awaits a 1-frame\n * yield (rAF on web / 16 ms setTimeout on RN/Hermes) so the re-renders'\n * `t()` calls repopulate the registry, then snapshots. When the i18next\n * instance isn't reachable (non-Verbumia host) returns the current\n * registry snapshot WITHOUT the reset+rerender — strictly better than a\n * false-empty, matches the pre-0.2.7 `scope: \"all\"` behavior.\n */\nasync function captureCurrentViewSnapshot(\n i18next:\n | {\n language?: string;\n changeLanguage?: (lng: string) => Promise<unknown>;\n }\n | undefined,\n): Promise<DeclaredKey[]> {\n type Reg = {\n snapshot: () => DeclaredKey[];\n reset?: () => void;\n };\n const reg = (globalThis as Record<string, unknown>)\n .__verbumia_key_registry__ as Reg | undefined;\n if (!reg) return [];\n const lng = i18next?.language;\n const change = i18next?.changeLanguage;\n if (typeof change === \"function\" && typeof lng === \"string\" && lng) {\n // Reset BEFORE the trigger so the post-rerender repopulation is the\n // only contributor to the snapshot.\n reg.reset?.();\n try {\n await change(lng);\n } catch {\n // Defensive — if i18next throws (e.g. exotic backend), fall\n // through to the no-trigger path: snapshot whatever's there.\n }\n await new Promise<void>((r) => {\n if (typeof requestAnimationFrame === \"function\") {\n requestAnimationFrame(() => r());\n } else {\n setTimeout(() => r(), 16);\n }\n });\n return reg.snapshot();\n }\n // No reachable i18next — degrade to \"snapshot the accumulator\". Avoids\n // a guaranteed false-empty on non-Verbumia hosts.\n return reg.snapshot();\n}\n\nexport function feedbackPlugin(options: FeedbackPluginOptions): I18nPlugin {\n const store = makeStore();\n let client: FeedbackClient | null = null;\n\n function Outlet() {\n const state = useSyncExternalStore(\n store.subscribe,\n store.getState,\n store.getState,\n );\n if (!state.isOpen || !client || typeof document === \"undefined\") return null;\n const c = client;\n // Precedence: explicit `options.keys` from the host wins (the\n // CONTRACT v5 \"authoritative declaration\"); else the 0.2.7\n // `scope: \"current-view\"` snapshot captured by `controller.open()`;\n // else the panel falls back to the live registry (the pre-0.2.7\n // path, equivalent to `scope: \"all\"`).\n const panelKeys = options.keys ?? state.snapshotKeys;\n return createPortal(\n <FeedbackPanel\n client={c}\n keys={panelKeys}\n namespace={options.namespace}\n tos={options.tos ?? \"modal\"}\n onClose={() => {\n store.setOpen(false);\n void c.flush();\n }}\n />,\n document.body,\n );\n }\n\n return {\n name: \"@verbumia/feedback\",\n setup(ctx) {\n // #806 SeedSower lang-change init source: prefer an explicit\n // plugin option, then the i18n provider's CURRENT language (so\n // mounts that happen after a boot-time language flip get it\n // right), then the boot-snapshot defaultLocale as the floor.\n const initialLanguage =\n options.language ?? ctx.i18n?.language ?? ctx.config.defaultLocale;\n // 0.2.7 ToS: `tos: \"skip\"` flips `autoAcceptTos: false` so the\n // client refuses to silently `POST /v1/feedback/tos` on the user's\n // behalf — the host owns acceptance via `controller.acceptTos()`.\n const tos: \"modal\" | \"skip\" = options.tos ?? \"modal\";\n client = new FeedbackClient({\n apiBase:\n options.apiBase ?? ctx.config.apiBase ?? \"https://api.verbumia.dev\",\n projectId: options.projectId ?? ctx.config.projectUuid,\n language: initialLanguage,\n endUserId: options.endUserId,\n fetchImpl: options.fetchImpl,\n apiKey: options.apiKey,\n autoAcceptTos: tos !== \"skip\",\n });\n const clientRef = client;\n // 0.2.7 addon-state cache. `null` until the first fetch resolves;\n // a transient transport error leaves the prior value in place\n // (best-effort — never flip the host's CTA on a network blip).\n let addonState: FeedbackAddonState | null = null;\n const cta: \"auto\" | \"show\" | \"hide\" = options.cta ?? \"auto\";\n // 0.2.7 — `scope: \"current-view\"` is the new default. The\n // accumulator behavior from 1.0.3 stays available behind\n // `scope: \"all\"` for hosts that want it (or for non-Verbumia hosts\n // where the changeLanguage trigger can't reach an i18next).\n const scope: \"current-view\" | \"all\" = options.scope ?? \"current-view\";\n const ctxI18next = ctx.i18n?.i18next;\n const refreshState = async (): Promise<void> => {\n try {\n const next = await clientRef.getAddonState();\n if (next !== null) addonState = next;\n } catch {\n // Pre-acceptTos + no apiKey → getAddonState throws synchronously\n // (\"addon state requires apiKey or acceptTos\"). Defer silently;\n // a later acceptTos() will let the plugin retry.\n }\n };\n // Fetch state at setup time when an apiKey is available. Without\n // one, the controller getters stay `null` until the host calls\n // controller.acceptTos() (or the modal does), at which point the\n // bearer path unblocks the fetch — wired below.\n if (options.apiKey) void refreshState();\n // #806 — re-sync the client (and refresh addon-state, since\n // enabledLanguages may differ by language) on runtime language\n // changes. Optional: pre-1.0.5 hosts no-op.\n let langUnsub: (() => void) | undefined;\n if (typeof ctx.onLanguageChange === \"function\") {\n langUnsub = ctx.onLanguageChange((lng) => {\n clientRef.setLanguage(lng);\n void refreshState();\n });\n }\n const controller: FeedbackController = {\n open: async (): Promise<void> => {\n if (scope === \"current-view\") {\n const snapshot = await captureCurrentViewSnapshot(ctxI18next);\n store.setOpen(true, snapshot);\n return;\n }\n // scope === \"all\" → pre-0.2.7 behavior: no reset, no rerender,\n // no snapshot — the panel reads the live registry directly.\n store.setOpen(true);\n },\n close: () => {\n store.setOpen(false);\n void clientRef?.flush();\n },\n client: clientRef,\n get tosVersion(): string {\n return clientRef.tosVersion;\n },\n get hasAcceptedTos(): boolean {\n return clientRef.hasAcceptedTos;\n },\n acceptTos: async (): Promise<void> => {\n await clientRef.acceptTos();\n // Newly-minted bearer unblocks the deferred state fetch.\n if (!options.apiKey) await refreshState();\n },\n get isActive(): boolean | null {\n if (cta === \"show\") return true;\n if (cta === \"hide\") return false;\n // cta === \"auto\"\n return addonState ? addonState.isActive : null;\n },\n get enabledLanguages(): string[] | \"all\" | null {\n return addonState ? addonState.enabledLanguages : null;\n },\n get sku(): \"feedback_starter\" | \"feedback_unlimited\" | null {\n return addonState ? addonState.sku : null;\n },\n };\n options.onReady?.(controller);\n if (options.controllerRef) options.controllerRef.current = controller;\n return () => {\n langUnsub?.();\n if (options.controllerRef) options.controllerRef.current = null;\n void client?.flush();\n };\n },\n render: () => <Outlet />,\n };\n}\n","import { useCallback, useEffect, useState } from \"react\";\n\nimport type { FeedbackClient } from \"../core/client\";\nimport { resolveKeys } from \"../core/keys\";\nimport { t as tr } from \"../core/locales\";\nimport type { DeclaredKey, FeedbackString } from \"../core/types\";\n\nconst C = {\n bg: \"#0b0f0e\",\n panel: \"#111714\",\n border: \"#1f2a25\",\n text: \"#e7f5ef\",\n dim: \"#8aa79b\",\n emerald: \"#10b981\",\n emeraldSoft: \"#34d399\",\n};\n\nexport function FeedbackPanel(props: {\n client: FeedbackClient;\n keys?: DeclaredKey[];\n namespace?: string | string[];\n /** 0.2.7 — `\"skip\"` removes the built-in ToS step entirely; the host\n * owns consent (via `controller.acceptTos()`). When the user has not\n * yet accepted, the strings fetch surfaces a 401-derived error in the\n * existing error row. */\n tos?: \"modal\" | \"skip\";\n onClose: () => void;\n}) {\n const { client, keys, namespace, tos = \"modal\", onClose } = props;\n // `tos: \"skip\"` short-circuits the local consent state so the panel\n // renders the strings section directly. The client refuses to\n // auto-acceptTos (autoAcceptTos:false on the plugin side), so a\n // missing token surfaces as the existing 401 error row.\n const [consented, setConsented] = useState(\n tos === \"skip\" ? true : client.hasConsented,\n );\n const [busy, setBusy] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [strings, setStrings] = useState<FeedbackString[]>([]);\n // 0.2.8 — \"Show original\" toggle. OFF by default; user opt-in to see\n // the source-locale rendering above each translated string. Local to\n // the panel mount — not persisted across opens (each open is a fresh\n // review session per the scope:\"current-view\" 0.2.7 model).\n const [showSource, setShowSource] = useState<boolean>(false);\n\n const loadStrings = useCallback(async () => {\n setBusy(true);\n setError(null);\n try {\n // On-screen scoping is NORMATIVE (spec ltm 373, CONTRACT v5): show\n // ONLY rendered keys. Empty -> \"no strings on this view\"; never\n // fall back to fetching the whole project.\n const resolved = resolveKeys(keys, namespace);\n if (!resolved.length) {\n // #806 SeedSower P1 — when the panel opens with NO explicit\n // `keys` prop (i.e. it depended on the i18n SDK's on-screen\n // registry), an empty resolve almost always means the host's\n // `useTranslation` imports went to `react-i18next` instead of\n // `@verbumia/react-i18next`. Either way, the keys never fed the\n // registry. Log a single, actionable hint so the integrator\n // doesn't waste a debug cycle on \"the widget is broken\".\n if (!keys || keys.length === 0) {\n // eslint-disable-next-line no-console\n console.warn(\n \"[verbumia/feedback] registry empty — are your useTranslation imports coming from @verbumia/react-i18next?\",\n );\n }\n setStrings([]);\n return;\n }\n const res = await client.getStrings({ keys: resolved });\n setStrings(res.strings);\n } catch (e) {\n setError(e instanceof Error ? e.message : \"Failed to load strings\");\n } finally {\n setBusy(false);\n }\n }, [client, keys, namespace]);\n\n useEffect(() => {\n if (consented) void loadStrings();\n }, [consented, loadStrings]);\n\n const accept = useCallback(async () => {\n setBusy(true);\n setError(null);\n try {\n await client.acceptTos();\n setConsented(true);\n } catch (e) {\n setError(e instanceof Error ? e.message : \"Could not accept the terms\");\n } finally {\n setBusy(false);\n }\n }, [client]);\n\n return (\n <div\n role=\"dialog\"\n aria-label=\"Translation feedback\"\n style={{\n position: \"fixed\",\n inset: 0,\n zIndex: 2147483600,\n background: \"rgba(0,0,0,.55)\",\n display: \"flex\",\n justifyContent: \"flex-end\",\n }}\n onClick={onClose}\n >\n <div\n onClick={(e) => e.stopPropagation()}\n style={{\n width: \"min(420px, 100%)\",\n height: \"100%\",\n background: C.bg,\n color: C.text,\n borderLeft: `1px solid ${C.border}`,\n display: \"flex\",\n flexDirection: \"column\",\n fontFamily: \"system-ui, sans-serif\",\n }}\n >\n <header\n style={{\n padding: \"16px 18px\",\n borderBottom: `1px solid ${C.border}`,\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n }}\n >\n <strong style={{ color: C.emeraldSoft }}>Translation feedback</strong>\n <button\n type=\"button\"\n onClick={onClose}\n aria-label=\"Close\"\n style={{\n background: \"transparent\",\n color: C.dim,\n border: \"none\",\n fontSize: 20,\n cursor: \"pointer\",\n }}\n >\n ×\n </button>\n </header>\n\n <div style={{ padding: 18, overflowY: \"auto\", flex: 1 }}>\n {error && (\n <p style={{ color: \"#f87171\", fontSize: 13 }}>{error}</p>\n )}\n\n {!consented ? (\n <ConsentStep\n version={client.tosVersion}\n busy={busy}\n onAccept={accept}\n />\n ) : busy && !strings.length ? (\n <p style={{ color: C.dim }}>Loading…</p>\n ) : !strings.length ? (\n <p style={{ color: C.dim }}>No strings to review on this view.</p>\n ) : (\n <>\n {/* 0.2.8 — source-language toggle at the top of the strings\n list. Bundled labels en/fr/es/de (core/locales.ts);\n falls back to en when the user's language has no entry. */}\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"flex-end\",\n marginBottom: 10,\n }}\n >\n <label\n style={{\n display: \"inline-flex\",\n alignItems: \"center\",\n gap: 8,\n color: C.dim,\n fontSize: 12,\n cursor: \"pointer\",\n }}\n >\n <input\n type=\"checkbox\"\n checked={showSource}\n onChange={(e) => setShowSource(e.target.checked)}\n />\n {tr(client.language, \"showOriginal\")}\n </label>\n </div>\n {strings.map((s) => (\n <StringRow\n key={`${s.namespace}:${s.key}`}\n s={s}\n client={client}\n showSource={showSource}\n />\n ))}\n </>\n )}\n </div>\n <footer\n style={{\n padding: \"10px 18px\",\n borderTop: `1px solid ${C.border}`,\n color: C.dim,\n fontSize: 11,\n }}\n >\n Powered by Verbumia\n </footer>\n </div>\n </div>\n );\n}\n\nfunction ConsentStep(props: {\n version?: string;\n busy: boolean;\n onAccept: () => void;\n}) {\n return (\n <div>\n <p style={{ lineHeight: 1.5, fontSize: 14 }}>\n Help improve the translations in this app. Your ratings and\n suggestions are sent to the app owner via Verbumia. Don’t submit\n personal or sensitive information.\n </p>\n <button\n type=\"button\"\n disabled={props.busy}\n onClick={props.onAccept}\n style={{\n marginTop: 12,\n width: \"100%\",\n background: C.emerald,\n color: \"#03110c\",\n border: \"none\",\n borderRadius: 8,\n padding: \"12px 14px\",\n fontWeight: 700,\n cursor: props.busy ? \"default\" : \"pointer\",\n opacity: props.busy ? 0.6 : 1,\n }}\n >\n {props.busy\n ? \"…\"\n : `I accept the terms${props.version ? ` (v${props.version})` : \"\"}`}\n </button>\n </div>\n );\n}\n\nfunction StringRow(props: {\n s: FeedbackString;\n client: FeedbackClient;\n /** 0.2.8 — whether the panel-level \"Show original\" toggle is on.\n * The source row is only rendered when this is `true` AND the\n * source locale differs from the target locale (so a single-locale\n * project doesn't display the same string twice). */\n showSource: boolean;\n}) {\n const { s, client, showSource } = props;\n const [mine, setMine] = useState<number | null>(s.my_rating);\n const [showSuggest, setShowSuggest] = useState(false);\n // 0.2.8 — when an existing `my_suggestion` came back from /strings,\n // the editor pre-fills with that text and the submit path switches\n // from POST to PATCH. The id is captured here (not in useState) so\n // it's stable for the lifetime of this StringRow mount; the batched\n // POST endpoint doesn't return per-item ids today, so we don't\n // promote a fresh create into edit mode mid-session — the user gets\n // edit mode on the NEXT panel open (when /strings re-returns the\n // server-persisted my_suggestion).\n const mySuggestionId: string | null = s.my_suggestion\n ? s.my_suggestion.id\n : null;\n const [text, setText] = useState<string>(s.my_suggestion?.text ?? \"\");\n const [sent, setSent] = useState(false);\n const [editError, setEditError] = useState<string | null>(null);\n\n const rate = (stars: number) => {\n setMine(stars);\n client.rate({\n namespace: s.namespace,\n key: s.key,\n language: client.language,\n translation_hash: s.translation_hash,\n stars,\n });\n };\n\n const submitSuggestion = async () => {\n const trimmed = text.trim();\n if (!trimmed) return;\n setEditError(null);\n try {\n if (mySuggestionId) {\n // 0.2.8 edit path — PATCH /v1/feedback/suggestions/{id}.\n // Synchronous round-trip (not the batched queue) so the user\n // gets immediate confirmation of the edit landing.\n await client.editSuggestion(mySuggestionId, trimmed);\n } else {\n // Legacy create path — batched (queued + debounced).\n client.suggest({\n namespace: s.namespace,\n key: s.key,\n language: client.language,\n translation_hash: s.translation_hash,\n suggested_text: trimmed,\n });\n }\n setSent(true);\n setShowSuggest(false);\n } catch (e) {\n setEditError(\n e instanceof Error ? e.message : \"Could not update the suggestion\",\n );\n }\n };\n\n // 0.2.8 source row — only shown when the toggle is on AND we'd be\n // displaying something the user can't already see in `s.value`.\n const showSourceRow = showSource && s.source_locale !== client.language;\n // Submit label adapts to the mode the editor is in. Bundled in\n // core/locales.ts so the SDK ships labels for at least en/fr/es.\n const submitLabel = tr(\n client.language,\n mySuggestionId ? \"updateMySuggestion\" : \"submitSuggestion\",\n );\n\n return (\n <div\n style={{\n background: C.panel,\n border: `1px solid ${C.border}`,\n borderRadius: 10,\n padding: 12,\n marginBottom: 10,\n }}\n >\n <div style={{ fontSize: 12, color: C.dim }}>\n {s.namespace} · {s.key}\n </div>\n {showSourceRow ? (\n <div\n data-testid=\"source-row\"\n style={{\n margin: \"4px 0 2px\",\n fontSize: 12,\n color: C.dim,\n fontStyle: \"italic\",\n }}\n >\n {s.source_text}\n </div>\n ) : null}\n <div style={{ margin: \"6px 0 10px\", fontSize: 15 }}>{s.value}</div>\n <div style={{ display: \"flex\", gap: 4 }}>\n {[1, 2, 3, 4, 5].map((n) => (\n <button\n key={n}\n type=\"button\"\n aria-label={`${n} star${n > 1 ? \"s\" : \"\"}`}\n onClick={() => rate(n)}\n style={{\n background: \"transparent\",\n border: \"none\",\n cursor: \"pointer\",\n fontSize: 20,\n color: mine && n <= mine ? C.emeraldSoft : C.border,\n }}\n >\n ★\n </button>\n ))}\n <button\n type=\"button\"\n onClick={() => setShowSuggest((v) => !v)}\n style={{\n marginLeft: \"auto\",\n background: \"transparent\",\n color: C.emeraldSoft,\n border: `1px solid ${C.border}`,\n borderRadius: 6,\n padding: \"4px 10px\",\n fontSize: 12,\n cursor: \"pointer\",\n }}\n >\n {sent ? \"Suggested ✓\" : submitLabel}\n </button>\n </div>\n {showSuggest && (\n <div style={{ marginTop: 10 }}>\n <textarea\n value={text}\n onChange={(e) => setText(e.target.value)}\n rows={3}\n placeholder=\"Your suggested translation…\"\n style={{\n width: \"100%\",\n background: C.bg,\n color: C.text,\n border: `1px solid ${C.border}`,\n borderRadius: 6,\n padding: 8,\n fontSize: 14,\n resize: \"vertical\",\n }}\n />\n {editError ? (\n <p style={{ color: \"#f87171\", fontSize: 12, marginTop: 6 }}>\n {editError}\n </p>\n ) : null}\n <button\n type=\"button\"\n onClick={() => void submitSuggestion()}\n style={{\n marginTop: 6,\n background: C.emerald,\n color: \"#03110c\",\n border: \"none\",\n borderRadius: 6,\n padding: \"8px 12px\",\n fontWeight: 700,\n cursor: \"pointer\",\n }}\n >\n {submitLabel}\n </button>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["jsx"]}
@@ -75,11 +75,61 @@ var FeedbackClient = class {
75
75
  get hasConsented() {
76
76
  return this.tokens !== null;
77
77
  }
78
+ /** Alias of `hasConsented` exposed under the 0.2.7 naming used by the
79
+ * `controller.hasAcceptedTos` getter (the SDK's built-in modal and a
80
+ * host's external ToS page share the same persisted token bundle, so
81
+ * both flip this boolean once a session is bootstrapped). */
82
+ get hasAcceptedTos() {
83
+ return this.tokens !== null;
84
+ }
78
85
  /** Server-minted sessionId / grouping_key (from the token bundle).
79
86
  * Available only after `acceptTos()`; never client-generated. */
80
87
  get sessionId() {
81
88
  return this.tokens?.grouping_key;
82
89
  }
90
+ /**
91
+ * 0.2.7 — `GET /v1/projects/{projectId}/feedback-addon/state`.
92
+ *
93
+ * Auth selection (matches backend's dual-acceptance after task 836's
94
+ * follow-up PR — see CONTRACT note in the response type):
95
+ * - If `cfg.apiKey` is set → `Authorization: ApiKey <key>` (so the
96
+ * plugin can fetch state at `setup()` time, before the user has
97
+ * accepted ToS).
98
+ * - Else if a user-session bundle exists → `Authorization: Bearer
99
+ * <access_token>` (the deferred-after-acceptTos path).
100
+ * - Else → throw `FeedbackError("addon state requires apiKey or
101
+ * acceptTos")`. The plugin's setup catches and surfaces a
102
+ * console.warn the first time, then re-attempts on the
103
+ * bundle-mint that follows `acceptTos()`.
104
+ *
105
+ * Best-effort: a transport error returns `null` instead of throwing,
106
+ * so the controller's `isActive` falls back to whatever was last
107
+ * known (or its initial value) without flipping the host's CTA into
108
+ * an inconsistent state on a transient blip.
109
+ */
110
+ async getAddonState() {
111
+ let auth;
112
+ if (this.cfg.apiKey) {
113
+ auth = `ApiKey ${this.cfg.apiKey}`;
114
+ } else if (this.tokens) {
115
+ auth = `Bearer ${this.tokens.access_token}`;
116
+ } else {
117
+ throw new FeedbackError(
118
+ "addon state requires apiKey or acceptTos"
119
+ );
120
+ }
121
+ const url = `${this.base()}/v1/projects/${this.cfg.projectId}/feedback-addon/state`;
122
+ try {
123
+ const res = await this.fetchImpl(url, {
124
+ method: "GET",
125
+ headers: { Authorization: auth }
126
+ });
127
+ if (!res.ok) return null;
128
+ return await res.json();
129
+ } catch {
130
+ return null;
131
+ }
132
+ }
83
133
  /** BCP-47 language the widget is rating strings in. */
84
134
  get language() {
85
135
  return this.cfg.language;
@@ -151,9 +201,18 @@ var FeedbackClient = class {
151
201
  }
152
202
  this.tokens = await res.json();
153
203
  }
154
- /** Authenticated fetch with a single transparent refresh-on-401 retry. */
204
+ /** Authenticated fetch with a single transparent refresh-on-401 retry.
205
+ * When `cfg.autoAcceptTos === false` (the plugin's `tos: "skip"` opt-in
206
+ * / 0.2.7) and no token has been minted yet, throws a `not consented`
207
+ * error instead of silently calling `acceptTos()` — the host promises
208
+ * to handle consent externally via `controller.acceptTos()`. */
155
209
  async authed(path, init, retry = true) {
156
- if (!this.tokens) await this.acceptTos();
210
+ if (!this.tokens) {
211
+ if (this.cfg.autoAcceptTos === false) {
212
+ throw new FeedbackError("not consented");
213
+ }
214
+ await this.acceptTos();
215
+ }
157
216
  const res = await this.fetchImpl(`${this.base()}${path}`, {
158
217
  ...init,
159
218
  headers: {
@@ -190,6 +249,31 @@ var FeedbackClient = class {
190
249
  suggest(payload) {
191
250
  this.enqueue({ kind: "suggestion", payload });
192
251
  }
252
+ /**
253
+ * 0.2.8 — `PATCH /v1/feedback/suggestions/{id}` (backend task 847,
254
+ * deploy `45190c8`). Used by the panel's edit-mode editor when the
255
+ * end-user already has a pending suggestion for a string (the
256
+ * `FeedbackString.my_suggestion.id` from a prior submit). Submits
257
+ * synchronously rather than going through the rating/suggestion
258
+ * batch queue — the host triggered an explicit edit, not a passive
259
+ * rating, so we want the round-trip to surface before the panel
260
+ * closes. Best-effort failures bubble; the caller catches and
261
+ * shows an error row in the panel.
262
+ *
263
+ * Wire body: `{ text: string }` (backend probe confirmed). NOT the
264
+ * legacy `{ suggested_text }` of the batched POST path.
265
+ */
266
+ async editSuggestion(id, text) {
267
+ if (!id) throw new FeedbackError("editSuggestion: id is required");
268
+ const trimmed = (text ?? "").trim();
269
+ if (!trimmed) throw new FeedbackError("editSuggestion: text is required");
270
+ const res = await this.authed(`/v1/feedback/suggestions/${encodeURIComponent(id)}`, {
271
+ method: "PATCH",
272
+ headers: { "Content-Type": "application/json" },
273
+ body: JSON.stringify({ text: trimmed })
274
+ });
275
+ if (!res.ok) throw await this.problem(res, "failed to update suggestion");
276
+ }
193
277
  enqueue(item) {
194
278
  this.queue.push(item);
195
279
  if (this.queue.length >= this.cfg.maxBatch) {