@verbumia/feedback 0.2.7 → 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-7KWEI55W.js → chunk-EMGN6MR4.js} +26 -1
  4. package/dist/chunk-EMGN6MR4.js.map +1 -0
  5. package/dist/{client-D83qhH0O.d.cts → client-CnEK_2SD.d.cts} +41 -0
  6. package/dist/{client-D83qhH0O.d.ts → client-CnEK_2SD.d.ts} +41 -0
  7. package/dist/core/index.cjs +25 -0
  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-D4oJtn64.d.cts → keys-2_T5bDpX.d.cts} +1 -1
  13. package/dist/{keys-BpVB1kcI.d.ts → keys-eHc_lx5v.d.ts} +1 -1
  14. package/dist/native/index.cjs +155 -25
  15. package/dist/native/index.cjs.map +1 -1
  16. package/dist/native/index.d.cts +3 -3
  17. package/dist/native/index.d.ts +3 -3
  18. package/dist/native/index.js +99 -27
  19. package/dist/native/index.js.map +1 -1
  20. package/dist/react/index.cjs +157 -18
  21. package/dist/react/index.cjs.map +1 -1
  22. package/dist/react/index.d.cts +3 -3
  23. package/dist/react/index.d.ts +3 -3
  24. package/dist/react/index.js +101 -20
  25. package/dist/react/index.js.map +1 -1
  26. package/dist/svelte/index.cjs +25 -0
  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 +25 -0
  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-7KWEI55W.js.map +0 -1
@@ -1,3 +1,6 @@
1
+ import {
2
+ t
3
+ } from "../chunk-EKU6NNWI.js";
1
4
  import "../chunk-5NA2TFPG.js";
2
5
  import {
3
6
  FeedbackClient,
@@ -5,7 +8,7 @@ import {
5
8
  __require,
6
9
  hasKeyRegistry,
7
10
  resolveKeys
8
- } from "../chunk-7KWEI55W.js";
11
+ } from "../chunk-EMGN6MR4.js";
9
12
 
10
13
  // src/native/plugin.tsx
11
14
  import { useSyncExternalStore } from "react";
@@ -22,7 +25,7 @@ import {
22
25
  TouchableOpacity,
23
26
  View
24
27
  } from "react-native";
25
- import { jsx, jsxs } from "react/jsx-runtime";
28
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
26
29
  var SafeAreaView;
27
30
  try {
28
31
  const mod = __require("react-native-safe-area-context");
@@ -51,6 +54,7 @@ function FeedbackModal(props) {
51
54
  const [busy, setBusy] = useState(false);
52
55
  const [error, setError] = useState(null);
53
56
  const [strings, setStrings] = useState([]);
57
+ const [showSource, setShowSource] = useState(false);
54
58
  const loadStrings = useCallback(async () => {
55
59
  setBusy(true);
56
60
  setError(null);
@@ -148,14 +152,49 @@ function FeedbackModal(props) {
148
152
  // to its intrinsic height and the panel left a large empty
149
153
  // area below the last row (the second half of the
150
154
  // SeedSower repro).
151
- /* @__PURE__ */ jsx(ScrollView, { style: { flex: 1 }, children: !strings.length ? /* @__PURE__ */ jsx(Text, { style: { color: C.dim }, children: busy ? "Loading\u2026" : "No strings to review on this view." }) : strings.map((s) => /* @__PURE__ */ jsx(
152
- StringRow,
153
- {
154
- s,
155
- client
156
- },
157
- `${s.namespace}:${s.key}`
158
- )) })
155
+ /* @__PURE__ */ jsx(ScrollView, { style: { flex: 1 }, children: !strings.length ? /* @__PURE__ */ jsx(Text, { style: { color: C.dim }, children: busy ? "Loading\u2026" : "No strings to review on this view." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
156
+ /* @__PURE__ */ jsxs(
157
+ TouchableOpacity,
158
+ {
159
+ onPress: () => setShowSource((v) => !v),
160
+ accessibilityLabel: t(client.language, "showOriginal"),
161
+ style: {
162
+ alignSelf: "flex-end",
163
+ flexDirection: "row",
164
+ alignItems: "center",
165
+ marginBottom: 10,
166
+ paddingVertical: 4,
167
+ paddingHorizontal: 8,
168
+ borderWidth: 1,
169
+ borderColor: C.border,
170
+ borderRadius: 6
171
+ },
172
+ children: [
173
+ /* @__PURE__ */ jsx(
174
+ Text,
175
+ {
176
+ style: {
177
+ color: showSource ? C.emeraldSoft : C.dim,
178
+ fontSize: 12,
179
+ marginRight: 6
180
+ },
181
+ children: showSource ? "\u2611" : "\u2610"
182
+ }
183
+ ),
184
+ /* @__PURE__ */ jsx(Text, { style: { color: C.dim, fontSize: 12 }, children: t(client.language, "showOriginal") })
185
+ ]
186
+ }
187
+ ),
188
+ strings.map((s) => /* @__PURE__ */ jsx(
189
+ StringRow,
190
+ {
191
+ s,
192
+ client,
193
+ showSource
194
+ },
195
+ `${s.namespace}:${s.key}`
196
+ ))
197
+ ] }) })
159
198
  )
160
199
  ] })
161
200
  }
@@ -166,11 +205,42 @@ function FeedbackModal(props) {
166
205
  );
167
206
  }
168
207
  function StringRow(props) {
169
- const { s, client } = props;
208
+ const { s, client, showSource } = props;
170
209
  const [mine, setMine] = useState(s.my_rating);
171
210
  const [show, setShow] = useState(false);
172
- const [text, setText] = useState("");
211
+ const mySuggestionId = s.my_suggestion ? s.my_suggestion.id : null;
212
+ const [text, setText] = useState(s.my_suggestion?.text ?? "");
173
213
  const [sent, setSent] = useState(false);
214
+ const [editError, setEditError] = useState(null);
215
+ const submit = async () => {
216
+ const trimmed = text.trim();
217
+ if (!trimmed) return;
218
+ setEditError(null);
219
+ try {
220
+ if (mySuggestionId) {
221
+ await client.editSuggestion(mySuggestionId, trimmed);
222
+ } else {
223
+ client.suggest({
224
+ namespace: s.namespace,
225
+ key: s.key,
226
+ language: client.language,
227
+ translation_hash: s.translation_hash,
228
+ suggested_text: trimmed
229
+ });
230
+ }
231
+ setSent(true);
232
+ setShow(false);
233
+ } catch (e) {
234
+ setEditError(
235
+ e instanceof Error ? e.message : "Could not update the suggestion"
236
+ );
237
+ }
238
+ };
239
+ const showSourceRow = showSource && s.source_locale !== client.language;
240
+ const submitLabel = t(
241
+ client.language,
242
+ mySuggestionId ? "updateMySuggestion" : "submitSuggestion"
243
+ );
174
244
  return /* @__PURE__ */ jsxs(
175
245
  View,
176
246
  {
@@ -188,6 +258,19 @@ function StringRow(props) {
188
258
  " \xB7 ",
189
259
  s.key
190
260
  ] }),
261
+ showSourceRow ? /* @__PURE__ */ jsx(
262
+ Text,
263
+ {
264
+ accessibilityLabel: "source",
265
+ style: {
266
+ color: C.dim,
267
+ fontSize: 12,
268
+ fontStyle: "italic",
269
+ marginTop: 4
270
+ },
271
+ children: s.source_text
272
+ }
273
+ ) : null,
191
274
  /* @__PURE__ */ jsx(Text, { style: { color: C.text, fontSize: 15, marginVertical: 6 }, children: s.value }),
192
275
  /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row" }, children: [
193
276
  [1, 2, 3, 4, 5].map((n) => /* @__PURE__ */ jsx(
@@ -223,7 +306,7 @@ function StringRow(props) {
223
306
  {
224
307
  onPress: () => setShow(!show),
225
308
  style: { marginLeft: "auto" },
226
- children: /* @__PURE__ */ jsx(Text, { style: { color: C.emeraldSoft }, children: sent ? "Suggested \u2713" : "Suggest" })
309
+ children: /* @__PURE__ */ jsx(Text, { style: { color: C.emeraldSoft }, children: sent ? "Suggested \u2713" : submitLabel })
227
310
  }
228
311
  )
229
312
  ] }),
@@ -247,29 +330,18 @@ function StringRow(props) {
247
330
  }
248
331
  }
249
332
  ),
333
+ editError ? /* @__PURE__ */ jsx(Text, { style: { color: "#f87171", fontSize: 12, marginTop: 6 }, children: editError }) : null,
250
334
  /* @__PURE__ */ jsx(
251
335
  TouchableOpacity,
252
336
  {
253
- onPress: () => {
254
- if (!text.trim()) return;
255
- client.suggest({
256
- namespace: s.namespace,
257
- key: s.key,
258
- language: client.language,
259
- translation_hash: s.translation_hash,
260
- suggested_text: text.trim()
261
- });
262
- setSent(true);
263
- setShow(false);
264
- setText("");
265
- },
337
+ onPress: () => void submit(),
266
338
  style: {
267
339
  marginTop: 6,
268
340
  backgroundColor: C.emerald,
269
341
  borderRadius: 6,
270
342
  padding: 10
271
343
  },
272
- children: /* @__PURE__ */ jsx(Text, { style: { color: "#03110c", fontWeight: "700", textAlign: "center" }, children: "Send suggestion" })
344
+ children: /* @__PURE__ */ jsx(Text, { style: { color: "#03110c", fontWeight: "700", textAlign: "center" }, children: submitLabel })
273
345
  }
274
346
  )
275
347
  ] }) : null
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/native/plugin.tsx","../../src/native/panel.tsx"],"sourcesContent":["/**\n * `@verbumia/feedback` (React Native / Expo) as a PLUGIN of the\n * `@verbumia/*-i18n` provider — same architecture as the web plugin\n * (task 599): no second context; isolated sibling outlet; imperative\n * controller for the host CTA; sessionId is server-minted. Native uses\n * an RN <Modal> (its own overlay) toggled by a private store, so the\n * host app never re-renders when feedback opens.\n */\nimport { useSyncExternalStore, type ReactNode } from \"react\";\n\nimport { FeedbackClient } from \"../core/client\";\nimport type { DeclaredKey, FeedbackAddonState } from \"../core/types\";\nimport { FeedbackModal } from \"./panel\";\n\nexport interface I18nPluginContext {\n i18n?: {\n language?: string;\n /** 0.2.7 — direct i18next handle used by the `scope: \"current-view\"`\n * snapshot path (see /react plugin for full notes). Optional. */\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\n * VerbumiaPluginContext in `@verbumia/react-i18next` ≥1.0.5). Returns\n * an unsubscribe fn the plugin MUST call from teardown. Optional so a\n * pre-1.0.5 react-i18next falls back gracefully. */\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 /** From 0.2.7 `open()` is async (Promise<void>) so the\n * `scope: \"current-view\"` snapshot sequence can run before the modal\n * mounts. Hosts that don't await still work; the modal mounts when\n * the promise resolves. */\n open: () => Promise<void>;\n close: () => void;\n client: FeedbackClient;\n /** 0.2.7 — current ToS version. */\n readonly tosVersion: string;\n /** 0.2.7 — whether this end-user has a live session-token bundle. */\n readonly hasAcceptedTos: boolean;\n /** 0.2.7 — programmatically POST `/v1/feedback/tos` (idempotent). Used\n * by hosts that build their own ToS page (`tos: \"skip\"`). */\n acceptTos: () => Promise<void>;\n /** 0.2.7 — addon-state-derived flag (see /react plugin docs).\n * `null` before the first state fetch resolves. */\n readonly isActive: boolean | null;\n /** 0.2.7 — `state.enabledLanguages` (string[] | \"all\" | null). */\n readonly enabledLanguages: string[] | \"all\" | null;\n /** 0.2.7 — raw SKU code. */\n readonly sku: \"feedback_starter\" | \"feedback_unlimited\" | null;\n}\n\nexport interface FeedbackPluginOptions {\n // No `tosVersion` — SDK build-time constant (SDK_TOS_VERSION, task 616).\n language?: string;\n apiBase?: string;\n projectId?: string;\n endUserId?: string;\n keys?: DeclaredKey[];\n /** Optional namespace filter (CONTRACT v6, additive). Single ns or\n * list; applied AFTER rendered-scoping (`rendered ∩ namespace`).\n * Unset ⇒ all resolved keys (v5 behaviour). */\n namespace?: string | string[];\n onReady?: (controller: FeedbackController) => void;\n controllerRef?: { current: FeedbackController | null };\n fetchImpl?: typeof fetch;\n /** 0.2.7 — `\"modal\"` (default, BC) keeps the built-in ToS step;\n * `\"skip\"` removes it (host owns acceptance via\n * `controller.acceptTos()`); /strings 401s render via the existing\n * error state until acceptance lands. */\n tos?: \"modal\" | \"skip\";\n /** 0.2.7 — host's project API key (`vrb_live_…`, scope project:read);\n * enables addon-state fetch at setup time. */\n apiKey?: string;\n /** 0.2.7 — controls `controller.isActive` (see /react plugin docs). */\n cta?: \"auto\" | \"show\" | \"hide\";\n /** 0.2.7 — on-screen-key set the panel reads (see /react plugin\n * docs). Default `\"current-view\"` (controller.open() resets the\n * registry, force-emits a same-locale languageChanged, waits 1\n * frame, snapshots). `\"all\"` preserves the pre-0.2.7 accumulator\n * view. */\n scope?: \"current-view\" | \"all\";\n}\n\n/** 0.2.7 panel store — see /react plugin docs. */\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/** 0.2.7 — `scope: \"current-view\"` snapshot. See /react plugin for\n * the full rationale + known limitation. On native (Hermes) we yield\n * via `setTimeout(16)` since `requestAnimationFrame` is rarely\n * injected by Hermes runtimes. */\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 reg.reset?.();\n try {\n await change(lng);\n } catch {\n // best-effort\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 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 (!client) return null;\n const c = client;\n // Precedence: options.keys > scope='current-view' snapshot > registry.\n const panelKeys = options.keys ?? state.snapshotKeys;\n return (\n <FeedbackModal\n client={c}\n visible={state.isOpen}\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 );\n }\n\n return {\n name: \"@verbumia/feedback\",\n setup(ctx) {\n // #806 SeedSower lang-change init source — same precedence as the\n // /react plugin: explicit option, then current i18n language,\n // then boot defaultLocale.\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 — same semantics as the /react plugin.\n let addonState: FeedbackAddonState | null = null;\n const cta: \"auto\" | \"show\" | \"hide\" = options.cta ?? \"auto\";\n // 0.2.7 scope (default \"current-view\" — see plugin docs).\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 path — defer until acceptTos().\n }\n };\n if (options.apiKey) void refreshState();\n // #806 — re-sync the client + refresh addon-state on lang change.\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 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 if (!options.apiKey) await refreshState();\n },\n get isActive(): boolean | null {\n if (cta === \"show\") return true;\n if (cta === \"hide\") return false;\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 { createElement, useCallback, useEffect, useState, type ComponentType, type ReactNode } from \"react\";\nimport {\n KeyboardAvoidingView,\n Modal,\n Platform,\n ScrollView,\n Text,\n TextInput,\n TouchableOpacity,\n View,\n} from \"react-native\";\n\nimport type { FeedbackClient } from \"../core/client\";\nimport { resolveKeys } from \"../core/keys\";\nimport type { DeclaredKey, FeedbackString } from \"../core/types\";\n\n/**\n * Soft-resolve `react-native-safe-area-context`'s `SafeAreaView` (#806\n * SeedSower Android panel-layout bug). When the host installed it (Expo\n * apps ship it by default; bare RN apps that follow the standard setup do\n * too), the panel's bottom edge respects the system gesture bar / home\n * indicator. When it isn't installed (web bundles, RN apps that opted\n * out), fall back to a plain `View` — same children, same style, no\n * insets. The dependency is listed as an OPTIONAL `peerDependency` so the\n * npm install never warns on web-only consumers.\n */\ntype SafeAreaProps = { edges?: ReadonlyArray<string>; style?: unknown; children?: ReactNode };\n// Local `require` declaration so we don't pull @types/node into this\n// package. Metro / Node both inject `require` at module scope; in the\n// ESM build path tsup polyfills it via createRequire(import.meta.url).\ndeclare const require: (id: string) => unknown;\nlet SafeAreaView: ComponentType<SafeAreaProps>;\ntry {\n const mod = require(\"react-native-safe-area-context\") as {\n SafeAreaView: ComponentType<SafeAreaProps>;\n };\n SafeAreaView = mod.SafeAreaView;\n} catch {\n SafeAreaView = ({ children, style }: SafeAreaProps) =>\n createElement(\n View as ComponentType<{ style?: unknown; children?: ReactNode }>,\n { style },\n children,\n );\n}\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 FeedbackModal(props: {\n client: FeedbackClient;\n visible: boolean;\n keys?: DeclaredKey[];\n namespace?: string | string[];\n /** 0.2.7 — `\"skip\"` removes the built-in ToS step; the host owns\n * acceptance via `controller.acceptTos()`. A missing token surfaces\n * as a 401-derived error in the existing error row. */\n tos?: \"modal\" | \"skip\";\n onClose: () => void;\n}) {\n const { client, visible, keys, namespace, tos = \"modal\", onClose } = props;\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\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 — same diagnostic as the web panel: an empty\n // registry resolve with no explicit `keys` prop almost always\n // means the host's `useTranslation` imports went to\n // `react-i18next` instead of `@verbumia/react-i18next`.\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 (visible && consented) void loadStrings();\n }, [visible, consented, loadStrings]);\n\n const accept = useCallback(async () => {\n setBusy(true);\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 <Modal\n visible={visible}\n transparent\n animationType=\"slide\"\n onRequestClose={onClose}\n >\n {/*\n #806 SeedSower native panel layout — KeyboardAvoidingView wraps the\n whole overlay so the suggestion TextInput isn't covered by the soft\n keyboard. `padding` on iOS / `height` on Android matches the RN\n community convention; flex:1 keeps the overlay full-screen above\n the keyboard reservation.\n */}\n <KeyboardAvoidingView\n style={{ flex: 1 }}\n behavior={Platform.OS === \"ios\" ? \"padding\" : \"height\"}\n >\n <View style={{ flex: 1, backgroundColor: \"rgba(0,0,0,.55)\", justifyContent: \"flex-end\" }}>\n {/*\n Panel sizing — `height:'85%'` gives a CONCRETE 85% of the\n modal's parent (the full-screen overlay), which is what the\n ToS step / strings ScrollView need to flex against. The prior\n `maxHeight:'85%'` was a ceiling only: without a height/flex\n child, the panel collapsed to its ToS intrinsic content (~15%\n of screen) on RN 0.77.3 Android. `maxHeight` is kept as a\n backstop in case a future child wants to render shorter (it\n never exceeds 85%; it can render shorter if the content does).\n\n SafeAreaView edges={['bottom']} keeps the CTA off the Android\n gesture bar / iOS home indicator. Falls back to plain View\n when react-native-safe-area-context isn't installed.\n */}\n <SafeAreaView\n edges={[\"bottom\"]}\n style={{\n height: \"85%\",\n maxHeight: \"85%\",\n backgroundColor: C.bg,\n borderTopWidth: 1,\n borderColor: C.border,\n }}\n >\n <View style={{ flex: 1, padding: 18 }}>\n <View\n style={{\n flexDirection: \"row\",\n justifyContent: \"space-between\",\n marginBottom: 12,\n }}\n >\n <Text style={{ color: C.emeraldSoft, fontWeight: \"700\", fontSize: 16 }}>\n Translation feedback\n </Text>\n <TouchableOpacity onPress={onClose} accessibilityLabel=\"Close\">\n <Text style={{ color: C.dim, fontSize: 20 }}>×</Text>\n </TouchableOpacity>\n </View>\n\n {error ? (\n <Text style={{ color: \"#f87171\", marginBottom: 8 }}>{error}</Text>\n ) : null}\n\n {!consented ? (\n <View>\n <Text style={{ color: C.text, lineHeight: 21 }}>\n Help improve the translations in this app. Your ratings and\n suggestions are sent to the app owner via Verbumia. Don’t\n submit personal or sensitive information.\n </Text>\n <TouchableOpacity\n disabled={busy}\n onPress={accept}\n style={{\n marginTop: 14,\n backgroundColor: C.emerald,\n borderRadius: 8,\n padding: 14,\n }}\n >\n <Text style={{ color: \"#03110c\", fontWeight: \"700\", textAlign: \"center\" }}>\n {busy ? \"…\" : `I accept the terms (v${client.tosVersion})`}\n </Text>\n </TouchableOpacity>\n </View>\n ) : (\n // flex:1 makes the ScrollView fill the remaining panel\n // height after the header; without it the list collapsed\n // to its intrinsic height and the panel left a large empty\n // area below the last row (the second half of the\n // SeedSower repro).\n <ScrollView style={{ flex: 1 }}>\n {!strings.length ? (\n <Text style={{ color: C.dim }}>\n {busy ? \"Loading…\" : \"No strings to review on this view.\"}\n </Text>\n ) : (\n strings.map((s) => (\n <StringRow\n key={`${s.namespace}:${s.key}`}\n s={s}\n client={client}\n />\n ))\n )}\n </ScrollView>\n )}\n </View>\n </SafeAreaView>\n </View>\n </KeyboardAvoidingView>\n </Modal>\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 [show, setShow] = useState(false);\n const [text, setText] = useState(\"\");\n const [sent, setSent] = useState(false);\n\n return (\n <View\n style={{\n backgroundColor: C.panel,\n borderWidth: 1,\n borderColor: C.border,\n borderRadius: 10,\n padding: 12,\n marginBottom: 10,\n }}\n >\n <Text style={{ color: C.dim, fontSize: 12 }}>\n {s.namespace} · {s.key}\n </Text>\n <Text style={{ color: C.text, fontSize: 15, marginVertical: 6 }}>\n {s.value}\n </Text>\n <View style={{ flexDirection: \"row\" }}>\n {[1, 2, 3, 4, 5].map((n) => (\n <TouchableOpacity\n key={n}\n accessibilityLabel={`${n} stars`}\n onPress={() => {\n setMine(n);\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 >\n <Text\n style={{\n fontSize: 22,\n marginRight: 4,\n color: mine && n <= mine ? C.emeraldSoft : C.border,\n }}\n >\n ★\n </Text>\n </TouchableOpacity>\n ))}\n <TouchableOpacity\n onPress={() => setShow(!show)}\n style={{ marginLeft: \"auto\" }}\n >\n <Text style={{ color: C.emeraldSoft }}>\n {sent ? \"Suggested ✓\" : \"Suggest\"}\n </Text>\n </TouchableOpacity>\n </View>\n {show ? (\n <View style={{ marginTop: 10 }}>\n <TextInput\n value={text}\n onChangeText={setText}\n multiline\n numberOfLines={3}\n placeholder=\"Your suggested translation…\"\n placeholderTextColor={C.dim}\n style={{\n backgroundColor: C.bg,\n color: C.text,\n borderWidth: 1,\n borderColor: C.border,\n borderRadius: 6,\n padding: 8,\n }}\n />\n <TouchableOpacity\n onPress={() => {\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 setShow(false);\n setText(\"\");\n }}\n style={{\n marginTop: 6,\n backgroundColor: C.emerald,\n borderRadius: 6,\n padding: 10,\n }}\n >\n <Text style={{ color: \"#03110c\", fontWeight: \"700\", textAlign: \"center\" }}>\n Send suggestion\n </Text>\n </TouchableOpacity>\n </View>\n ) : null}\n </View>\n );\n}\n"],"mappings":"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","names":["jsx"]}
1
+ {"version":3,"sources":["../../src/native/plugin.tsx","../../src/native/panel.tsx"],"sourcesContent":["/**\n * `@verbumia/feedback` (React Native / Expo) as a PLUGIN of the\n * `@verbumia/*-i18n` provider — same architecture as the web plugin\n * (task 599): no second context; isolated sibling outlet; imperative\n * controller for the host CTA; sessionId is server-minted. Native uses\n * an RN <Modal> (its own overlay) toggled by a private store, so the\n * host app never re-renders when feedback opens.\n */\nimport { useSyncExternalStore, type ReactNode } from \"react\";\n\nimport { FeedbackClient } from \"../core/client\";\nimport type { DeclaredKey, FeedbackAddonState } from \"../core/types\";\nimport { FeedbackModal } from \"./panel\";\n\nexport interface I18nPluginContext {\n i18n?: {\n language?: string;\n /** 0.2.7 — direct i18next handle used by the `scope: \"current-view\"`\n * snapshot path (see /react plugin for full notes). Optional. */\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\n * VerbumiaPluginContext in `@verbumia/react-i18next` ≥1.0.5). Returns\n * an unsubscribe fn the plugin MUST call from teardown. Optional so a\n * pre-1.0.5 react-i18next falls back gracefully. */\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 /** From 0.2.7 `open()` is async (Promise<void>) so the\n * `scope: \"current-view\"` snapshot sequence can run before the modal\n * mounts. Hosts that don't await still work; the modal mounts when\n * the promise resolves. */\n open: () => Promise<void>;\n close: () => void;\n client: FeedbackClient;\n /** 0.2.7 — current ToS version. */\n readonly tosVersion: string;\n /** 0.2.7 — whether this end-user has a live session-token bundle. */\n readonly hasAcceptedTos: boolean;\n /** 0.2.7 — programmatically POST `/v1/feedback/tos` (idempotent). Used\n * by hosts that build their own ToS page (`tos: \"skip\"`). */\n acceptTos: () => Promise<void>;\n /** 0.2.7 — addon-state-derived flag (see /react plugin docs).\n * `null` before the first state fetch resolves. */\n readonly isActive: boolean | null;\n /** 0.2.7 — `state.enabledLanguages` (string[] | \"all\" | null). */\n readonly enabledLanguages: string[] | \"all\" | null;\n /** 0.2.7 — raw SKU code. */\n readonly sku: \"feedback_starter\" | \"feedback_unlimited\" | null;\n}\n\nexport interface FeedbackPluginOptions {\n // No `tosVersion` — SDK build-time constant (SDK_TOS_VERSION, task 616).\n language?: string;\n apiBase?: string;\n projectId?: string;\n endUserId?: string;\n keys?: DeclaredKey[];\n /** Optional namespace filter (CONTRACT v6, additive). Single ns or\n * list; applied AFTER rendered-scoping (`rendered ∩ namespace`).\n * Unset ⇒ all resolved keys (v5 behaviour). */\n namespace?: string | string[];\n onReady?: (controller: FeedbackController) => void;\n controllerRef?: { current: FeedbackController | null };\n fetchImpl?: typeof fetch;\n /** 0.2.7 — `\"modal\"` (default, BC) keeps the built-in ToS step;\n * `\"skip\"` removes it (host owns acceptance via\n * `controller.acceptTos()`); /strings 401s render via the existing\n * error state until acceptance lands. */\n tos?: \"modal\" | \"skip\";\n /** 0.2.7 — host's project API key (`vrb_live_…`, scope project:read);\n * enables addon-state fetch at setup time. */\n apiKey?: string;\n /** 0.2.7 — controls `controller.isActive` (see /react plugin docs). */\n cta?: \"auto\" | \"show\" | \"hide\";\n /** 0.2.7 — on-screen-key set the panel reads (see /react plugin\n * docs). Default `\"current-view\"` (controller.open() resets the\n * registry, force-emits a same-locale languageChanged, waits 1\n * frame, snapshots). `\"all\"` preserves the pre-0.2.7 accumulator\n * view. */\n scope?: \"current-view\" | \"all\";\n}\n\n/** 0.2.7 panel store — see /react plugin docs. */\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/** 0.2.7 — `scope: \"current-view\"` snapshot. See /react plugin for\n * the full rationale + known limitation. On native (Hermes) we yield\n * via `setTimeout(16)` since `requestAnimationFrame` is rarely\n * injected by Hermes runtimes. */\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 reg.reset?.();\n try {\n await change(lng);\n } catch {\n // best-effort\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 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 (!client) return null;\n const c = client;\n // Precedence: options.keys > scope='current-view' snapshot > registry.\n const panelKeys = options.keys ?? state.snapshotKeys;\n return (\n <FeedbackModal\n client={c}\n visible={state.isOpen}\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 );\n }\n\n return {\n name: \"@verbumia/feedback\",\n setup(ctx) {\n // #806 SeedSower lang-change init source — same precedence as the\n // /react plugin: explicit option, then current i18n language,\n // then boot defaultLocale.\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 — same semantics as the /react plugin.\n let addonState: FeedbackAddonState | null = null;\n const cta: \"auto\" | \"show\" | \"hide\" = options.cta ?? \"auto\";\n // 0.2.7 scope (default \"current-view\" — see plugin docs).\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 path — defer until acceptTos().\n }\n };\n if (options.apiKey) void refreshState();\n // #806 — re-sync the client + refresh addon-state on lang change.\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 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 if (!options.apiKey) await refreshState();\n },\n get isActive(): boolean | null {\n if (cta === \"show\") return true;\n if (cta === \"hide\") return false;\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 { createElement, useCallback, useEffect, useState, type ComponentType, type ReactNode } from \"react\";\nimport {\n KeyboardAvoidingView,\n Modal,\n Platform,\n ScrollView,\n Text,\n TextInput,\n TouchableOpacity,\n View,\n} from \"react-native\";\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\n/**\n * Soft-resolve `react-native-safe-area-context`'s `SafeAreaView` (#806\n * SeedSower Android panel-layout bug). When the host installed it (Expo\n * apps ship it by default; bare RN apps that follow the standard setup do\n * too), the panel's bottom edge respects the system gesture bar / home\n * indicator. When it isn't installed (web bundles, RN apps that opted\n * out), fall back to a plain `View` — same children, same style, no\n * insets. The dependency is listed as an OPTIONAL `peerDependency` so the\n * npm install never warns on web-only consumers.\n */\ntype SafeAreaProps = { edges?: ReadonlyArray<string>; style?: unknown; children?: ReactNode };\n// Local `require` declaration so we don't pull @types/node into this\n// package. Metro / Node both inject `require` at module scope; in the\n// ESM build path tsup polyfills it via createRequire(import.meta.url).\ndeclare const require: (id: string) => unknown;\nlet SafeAreaView: ComponentType<SafeAreaProps>;\ntry {\n const mod = require(\"react-native-safe-area-context\") as {\n SafeAreaView: ComponentType<SafeAreaProps>;\n };\n SafeAreaView = mod.SafeAreaView;\n} catch {\n SafeAreaView = ({ children, style }: SafeAreaProps) =>\n createElement(\n View as ComponentType<{ style?: unknown; children?: ReactNode }>,\n { style },\n children,\n );\n}\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 FeedbackModal(props: {\n client: FeedbackClient;\n visible: boolean;\n keys?: DeclaredKey[];\n namespace?: string | string[];\n /** 0.2.7 — `\"skip\"` removes the built-in ToS step; the host owns\n * acceptance via `controller.acceptTos()`. A missing token surfaces\n * as a 401-derived error in the existing error row. */\n tos?: \"modal\" | \"skip\";\n onClose: () => void;\n}) {\n const { client, visible, keys, namespace, tos = \"modal\", onClose } = props;\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; see /react panel\n // for the rationale + bundled labels (en/fr/es/de).\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 — same diagnostic as the web panel: an empty\n // registry resolve with no explicit `keys` prop almost always\n // means the host's `useTranslation` imports went to\n // `react-i18next` instead of `@verbumia/react-i18next`.\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 (visible && consented) void loadStrings();\n }, [visible, consented, loadStrings]);\n\n const accept = useCallback(async () => {\n setBusy(true);\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 <Modal\n visible={visible}\n transparent\n animationType=\"slide\"\n onRequestClose={onClose}\n >\n {/*\n #806 SeedSower native panel layout — KeyboardAvoidingView wraps the\n whole overlay so the suggestion TextInput isn't covered by the soft\n keyboard. `padding` on iOS / `height` on Android matches the RN\n community convention; flex:1 keeps the overlay full-screen above\n the keyboard reservation.\n */}\n <KeyboardAvoidingView\n style={{ flex: 1 }}\n behavior={Platform.OS === \"ios\" ? \"padding\" : \"height\"}\n >\n <View style={{ flex: 1, backgroundColor: \"rgba(0,0,0,.55)\", justifyContent: \"flex-end\" }}>\n {/*\n Panel sizing — `height:'85%'` gives a CONCRETE 85% of the\n modal's parent (the full-screen overlay), which is what the\n ToS step / strings ScrollView need to flex against. The prior\n `maxHeight:'85%'` was a ceiling only: without a height/flex\n child, the panel collapsed to its ToS intrinsic content (~15%\n of screen) on RN 0.77.3 Android. `maxHeight` is kept as a\n backstop in case a future child wants to render shorter (it\n never exceeds 85%; it can render shorter if the content does).\n\n SafeAreaView edges={['bottom']} keeps the CTA off the Android\n gesture bar / iOS home indicator. Falls back to plain View\n when react-native-safe-area-context isn't installed.\n */}\n <SafeAreaView\n edges={[\"bottom\"]}\n style={{\n height: \"85%\",\n maxHeight: \"85%\",\n backgroundColor: C.bg,\n borderTopWidth: 1,\n borderColor: C.border,\n }}\n >\n <View style={{ flex: 1, padding: 18 }}>\n <View\n style={{\n flexDirection: \"row\",\n justifyContent: \"space-between\",\n marginBottom: 12,\n }}\n >\n <Text style={{ color: C.emeraldSoft, fontWeight: \"700\", fontSize: 16 }}>\n Translation feedback\n </Text>\n <TouchableOpacity onPress={onClose} accessibilityLabel=\"Close\">\n <Text style={{ color: C.dim, fontSize: 20 }}>×</Text>\n </TouchableOpacity>\n </View>\n\n {error ? (\n <Text style={{ color: \"#f87171\", marginBottom: 8 }}>{error}</Text>\n ) : null}\n\n {!consented ? (\n <View>\n <Text style={{ color: C.text, lineHeight: 21 }}>\n Help improve the translations in this app. Your ratings and\n suggestions are sent to the app owner via Verbumia. Don’t\n submit personal or sensitive information.\n </Text>\n <TouchableOpacity\n disabled={busy}\n onPress={accept}\n style={{\n marginTop: 14,\n backgroundColor: C.emerald,\n borderRadius: 8,\n padding: 14,\n }}\n >\n <Text style={{ color: \"#03110c\", fontWeight: \"700\", textAlign: \"center\" }}>\n {busy ? \"…\" : `I accept the terms (v${client.tosVersion})`}\n </Text>\n </TouchableOpacity>\n </View>\n ) : (\n // flex:1 makes the ScrollView fill the remaining panel\n // height after the header; without it the list collapsed\n // to its intrinsic height and the panel left a large empty\n // area below the last row (the second half of the\n // SeedSower repro).\n <ScrollView style={{ flex: 1 }}>\n {!strings.length ? (\n <Text style={{ color: C.dim }}>\n {busy ? \"Loading…\" : \"No strings to review on this view.\"}\n </Text>\n ) : (\n <>\n {/* 0.2.8 — source-language toggle. Bundled labels\n en/fr/es/de (core/locales.ts). */}\n <TouchableOpacity\n onPress={() => setShowSource((v) => !v)}\n accessibilityLabel={tr(client.language, \"showOriginal\")}\n style={{\n alignSelf: \"flex-end\",\n flexDirection: \"row\",\n alignItems: \"center\",\n marginBottom: 10,\n paddingVertical: 4,\n paddingHorizontal: 8,\n borderWidth: 1,\n borderColor: C.border,\n borderRadius: 6,\n }}\n >\n <Text\n style={{\n color: showSource ? C.emeraldSoft : C.dim,\n fontSize: 12,\n marginRight: 6,\n }}\n >\n {showSource ? \"☑\" : \"☐\"}\n </Text>\n <Text style={{ color: C.dim, fontSize: 12 }}>\n {tr(client.language, \"showOriginal\")}\n </Text>\n </TouchableOpacity>\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 </ScrollView>\n )}\n </View>\n </SafeAreaView>\n </View>\n </KeyboardAvoidingView>\n </Modal>\n );\n}\n\nfunction StringRow(props: {\n s: FeedbackString;\n client: FeedbackClient;\n /** 0.2.8 — see /react panel for semantics. */\n showSource: boolean;\n}) {\n const { s, client, showSource } = props;\n const [mine, setMine] = useState<number | null>(s.my_rating);\n const [show, setShow] = useState(false);\n // 0.2.8 — pre-fill from server-persisted my_suggestion when present.\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 submit = async (): Promise<void> => {\n const trimmed = text.trim();\n if (!trimmed) return;\n setEditError(null);\n try {\n if (mySuggestionId) {\n // 0.2.8 edit path — synchronous PATCH so the user sees the\n // round-trip land (vs batched queue for fresh creates).\n await client.editSuggestion(mySuggestionId, trimmed);\n } else {\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 setShow(false);\n } catch (e) {\n setEditError(\n e instanceof Error ? e.message : \"Could not update the suggestion\",\n );\n }\n };\n\n const showSourceRow = showSource && s.source_locale !== client.language;\n const submitLabel = tr(\n client.language,\n mySuggestionId ? \"updateMySuggestion\" : \"submitSuggestion\",\n );\n\n return (\n <View\n style={{\n backgroundColor: C.panel,\n borderWidth: 1,\n borderColor: C.border,\n borderRadius: 10,\n padding: 12,\n marginBottom: 10,\n }}\n >\n <Text style={{ color: C.dim, fontSize: 12 }}>\n {s.namespace} · {s.key}\n </Text>\n {showSourceRow ? (\n <Text\n accessibilityLabel=\"source\"\n style={{\n color: C.dim,\n fontSize: 12,\n fontStyle: \"italic\",\n marginTop: 4,\n }}\n >\n {s.source_text}\n </Text>\n ) : null}\n <Text style={{ color: C.text, fontSize: 15, marginVertical: 6 }}>\n {s.value}\n </Text>\n <View style={{ flexDirection: \"row\" }}>\n {[1, 2, 3, 4, 5].map((n) => (\n <TouchableOpacity\n key={n}\n accessibilityLabel={`${n} stars`}\n onPress={() => {\n setMine(n);\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 >\n <Text\n style={{\n fontSize: 22,\n marginRight: 4,\n color: mine && n <= mine ? C.emeraldSoft : C.border,\n }}\n >\n ★\n </Text>\n </TouchableOpacity>\n ))}\n <TouchableOpacity\n onPress={() => setShow(!show)}\n style={{ marginLeft: \"auto\" }}\n >\n <Text style={{ color: C.emeraldSoft }}>\n {sent ? \"Suggested ✓\" : submitLabel}\n </Text>\n </TouchableOpacity>\n </View>\n {show ? (\n <View style={{ marginTop: 10 }}>\n <TextInput\n value={text}\n onChangeText={setText}\n multiline\n numberOfLines={3}\n placeholder=\"Your suggested translation…\"\n placeholderTextColor={C.dim}\n style={{\n backgroundColor: C.bg,\n color: C.text,\n borderWidth: 1,\n borderColor: C.border,\n borderRadius: 6,\n padding: 8,\n }}\n />\n {editError ? (\n <Text style={{ color: \"#f87171\", fontSize: 12, marginTop: 6 }}>\n {editError}\n </Text>\n ) : null}\n <TouchableOpacity\n onPress={() => void submit()}\n style={{\n marginTop: 6,\n backgroundColor: C.emerald,\n borderRadius: 6,\n padding: 10,\n }}\n >\n <Text style={{ color: \"#03110c\", fontWeight: \"700\", textAlign: \"center\" }}>\n {submitLabel}\n </Text>\n </TouchableOpacity>\n </View>\n ) : null}\n </View>\n );\n}\n"],"mappings":"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","names":["jsx"]}
@@ -255,6 +255,31 @@ var FeedbackClient = class {
255
255
  suggest(payload) {
256
256
  this.enqueue({ kind: "suggestion", payload });
257
257
  }
258
+ /**
259
+ * 0.2.8 — `PATCH /v1/feedback/suggestions/{id}` (backend task 847,
260
+ * deploy `45190c8`). Used by the panel's edit-mode editor when the
261
+ * end-user already has a pending suggestion for a string (the
262
+ * `FeedbackString.my_suggestion.id` from a prior submit). Submits
263
+ * synchronously rather than going through the rating/suggestion
264
+ * batch queue — the host triggered an explicit edit, not a passive
265
+ * rating, so we want the round-trip to surface before the panel
266
+ * closes. Best-effort failures bubble; the caller catches and
267
+ * shows an error row in the panel.
268
+ *
269
+ * Wire body: `{ text: string }` (backend probe confirmed). NOT the
270
+ * legacy `{ suggested_text }` of the batched POST path.
271
+ */
272
+ async editSuggestion(id, text) {
273
+ if (!id) throw new FeedbackError("editSuggestion: id is required");
274
+ const trimmed = (text ?? "").trim();
275
+ if (!trimmed) throw new FeedbackError("editSuggestion: text is required");
276
+ const res = await this.authed(`/v1/feedback/suggestions/${encodeURIComponent(id)}`, {
277
+ method: "PATCH",
278
+ headers: { "Content-Type": "application/json" },
279
+ body: JSON.stringify({ text: trimmed })
280
+ });
281
+ if (!res.ok) throw await this.problem(res, "failed to update suggestion");
282
+ }
258
283
  enqueue(item) {
259
284
  this.queue.push(item);
260
285
  if (this.queue.length >= this.cfg.maxBatch) {
@@ -350,6 +375,42 @@ function dedupe(keys) {
350
375
  return out;
351
376
  }
352
377
 
378
+ // src/core/locales.ts
379
+ var MESSAGES = {
380
+ en: {
381
+ showOriginal: "Show original",
382
+ submitSuggestion: "Submit a suggestion",
383
+ updateMySuggestion: "Update my suggestion"
384
+ },
385
+ fr: {
386
+ showOriginal: "Voir l'original",
387
+ submitSuggestion: "Soumettre une suggestion",
388
+ updateMySuggestion: "Mettre \xE0 jour ma suggestion"
389
+ },
390
+ es: {
391
+ showOriginal: "Ver el original",
392
+ submitSuggestion: "Enviar una sugerencia",
393
+ updateMySuggestion: "Actualizar mi sugerencia"
394
+ },
395
+ de: {
396
+ showOriginal: "Original anzeigen",
397
+ submitSuggestion: "Vorschlag einreichen",
398
+ updateMySuggestion: "Meinen Vorschlag aktualisieren"
399
+ }
400
+ };
401
+ function t(locale, key) {
402
+ if (locale) {
403
+ const exact = MESSAGES[locale];
404
+ if (exact) return exact[key];
405
+ const base = locale.split("-")[0];
406
+ if (base) {
407
+ const baseMsgs = MESSAGES[base];
408
+ if (baseMsgs) return baseMsgs[key];
409
+ }
410
+ }
411
+ return MESSAGES.en[key];
412
+ }
413
+
353
414
  // src/react/panel.tsx
354
415
  var import_jsx_runtime = require("react/jsx-runtime");
355
416
  var C = {
@@ -369,6 +430,7 @@ function FeedbackPanel(props) {
369
430
  const [busy, setBusy] = (0, import_react.useState)(false);
370
431
  const [error, setError] = (0, import_react.useState)(null);
371
432
  const [strings, setStrings] = (0, import_react.useState)([]);
433
+ const [showSource, setShowSource] = (0, import_react.useState)(false);
372
434
  const loadStrings = (0, import_react.useCallback)(async () => {
373
435
  setBusy(true);
374
436
  setError(null);
@@ -475,7 +537,52 @@ function FeedbackPanel(props) {
475
537
  busy,
476
538
  onAccept: accept
477
539
  }
478
- ) : 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}`))
540
+ ) : 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." }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
541
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
542
+ "div",
543
+ {
544
+ style: {
545
+ display: "flex",
546
+ alignItems: "center",
547
+ justifyContent: "flex-end",
548
+ marginBottom: 10
549
+ },
550
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
551
+ "label",
552
+ {
553
+ style: {
554
+ display: "inline-flex",
555
+ alignItems: "center",
556
+ gap: 8,
557
+ color: C.dim,
558
+ fontSize: 12,
559
+ cursor: "pointer"
560
+ },
561
+ children: [
562
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
563
+ "input",
564
+ {
565
+ type: "checkbox",
566
+ checked: showSource,
567
+ onChange: (e) => setShowSource(e.target.checked)
568
+ }
569
+ ),
570
+ t(client.language, "showOriginal")
571
+ ]
572
+ }
573
+ )
574
+ }
575
+ ),
576
+ strings.map((s) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
577
+ StringRow,
578
+ {
579
+ s,
580
+ client,
581
+ showSource
582
+ },
583
+ `${s.namespace}:${s.key}`
584
+ ))
585
+ ] })
479
586
  ] }),
480
587
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
481
588
  "footer",
@@ -522,11 +629,13 @@ function ConsentStep(props) {
522
629
  ] });
523
630
  }
524
631
  function StringRow(props) {
525
- const { s, client } = props;
632
+ const { s, client, showSource } = props;
526
633
  const [mine, setMine] = (0, import_react.useState)(s.my_rating);
527
634
  const [showSuggest, setShowSuggest] = (0, import_react.useState)(false);
528
- const [text, setText] = (0, import_react.useState)("");
635
+ const mySuggestionId = s.my_suggestion ? s.my_suggestion.id : null;
636
+ const [text, setText] = (0, import_react.useState)(s.my_suggestion?.text ?? "");
529
637
  const [sent, setSent] = (0, import_react.useState)(false);
638
+ const [editError, setEditError] = (0, import_react.useState)(null);
530
639
  const rate = (stars) => {
531
640
  setMine(stars);
532
641
  client.rate({
@@ -537,19 +646,35 @@ function StringRow(props) {
537
646
  stars
538
647
  });
539
648
  };
540
- const submitSuggestion = () => {
541
- if (!text.trim()) return;
542
- client.suggest({
543
- namespace: s.namespace,
544
- key: s.key,
545
- language: client.language,
546
- translation_hash: s.translation_hash,
547
- suggested_text: text.trim()
548
- });
549
- setSent(true);
550
- setShowSuggest(false);
551
- setText("");
649
+ const submitSuggestion = async () => {
650
+ const trimmed = text.trim();
651
+ if (!trimmed) return;
652
+ setEditError(null);
653
+ try {
654
+ if (mySuggestionId) {
655
+ await client.editSuggestion(mySuggestionId, trimmed);
656
+ } else {
657
+ client.suggest({
658
+ namespace: s.namespace,
659
+ key: s.key,
660
+ language: client.language,
661
+ translation_hash: s.translation_hash,
662
+ suggested_text: trimmed
663
+ });
664
+ }
665
+ setSent(true);
666
+ setShowSuggest(false);
667
+ } catch (e) {
668
+ setEditError(
669
+ e instanceof Error ? e.message : "Could not update the suggestion"
670
+ );
671
+ }
552
672
  };
673
+ const showSourceRow = showSource && s.source_locale !== client.language;
674
+ const submitLabel = t(
675
+ client.language,
676
+ mySuggestionId ? "updateMySuggestion" : "submitSuggestion"
677
+ );
553
678
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
554
679
  "div",
555
680
  {
@@ -566,6 +691,19 @@ function StringRow(props) {
566
691
  " \xB7 ",
567
692
  s.key
568
693
  ] }),
694
+ showSourceRow ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
695
+ "div",
696
+ {
697
+ "data-testid": "source-row",
698
+ style: {
699
+ margin: "4px 0 2px",
700
+ fontSize: 12,
701
+ color: C.dim,
702
+ fontStyle: "italic"
703
+ },
704
+ children: s.source_text
705
+ }
706
+ ) : null,
569
707
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { margin: "6px 0 10px", fontSize: 15 }, children: s.value }),
570
708
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 4 }, children: [
571
709
  [1, 2, 3, 4, 5].map((n) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -600,7 +738,7 @@ function StringRow(props) {
600
738
  fontSize: 12,
601
739
  cursor: "pointer"
602
740
  },
603
- children: sent ? "Suggested \u2713" : "Suggest"
741
+ children: sent ? "Suggested \u2713" : submitLabel
604
742
  }
605
743
  )
606
744
  ] }),
@@ -624,11 +762,12 @@ function StringRow(props) {
624
762
  }
625
763
  }
626
764
  ),
765
+ editError ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { color: "#f87171", fontSize: 12, marginTop: 6 }, children: editError }) : null,
627
766
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
628
767
  "button",
629
768
  {
630
769
  type: "button",
631
- onClick: submitSuggestion,
770
+ onClick: () => void submitSuggestion(),
632
771
  style: {
633
772
  marginTop: 6,
634
773
  background: C.emerald,
@@ -639,7 +778,7 @@ function StringRow(props) {
639
778
  fontWeight: 700,
640
779
  cursor: "pointer"
641
780
  },
642
- children: "Send suggestion"
781
+ children: submitLabel
643
782
  }
644
783
  )
645
784
  ] })