@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.
- package/dist/chunk-EKU6NNWI.js +40 -0
- package/dist/chunk-EKU6NNWI.js.map +1 -0
- package/dist/{chunk-7KWEI55W.js → chunk-EMGN6MR4.js} +26 -1
- package/dist/chunk-EMGN6MR4.js.map +1 -0
- package/dist/{client-D83qhH0O.d.cts → client-CnEK_2SD.d.cts} +41 -0
- package/dist/{client-D83qhH0O.d.ts → client-CnEK_2SD.d.ts} +41 -0
- package/dist/core/index.cjs +25 -0
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +2 -2
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +1 -1
- package/dist/{keys-D4oJtn64.d.cts → keys-2_T5bDpX.d.cts} +1 -1
- package/dist/{keys-BpVB1kcI.d.ts → keys-eHc_lx5v.d.ts} +1 -1
- package/dist/native/index.cjs +155 -25
- package/dist/native/index.cjs.map +1 -1
- package/dist/native/index.d.cts +3 -3
- package/dist/native/index.d.ts +3 -3
- package/dist/native/index.js +99 -27
- package/dist/native/index.js.map +1 -1
- package/dist/react/index.cjs +157 -18
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +3 -3
- package/dist/react/index.d.ts +3 -3
- package/dist/react/index.js +101 -20
- package/dist/react/index.js.map +1 -1
- package/dist/svelte/index.cjs +25 -0
- package/dist/svelte/index.cjs.map +1 -1
- package/dist/svelte/index.d.cts +2 -2
- package/dist/svelte/index.d.ts +2 -2
- package/dist/svelte/index.js +1 -1
- package/dist/vue/index.cjs +25 -0
- package/dist/vue/index.cjs.map +1 -1
- package/dist/vue/index.d.cts +2 -2
- package/dist/vue/index.d.ts +2 -2
- package/dist/vue/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-7KWEI55W.js.map +0 -1
package/dist/native/index.js
CHANGED
|
@@ -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-
|
|
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." }) :
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
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" :
|
|
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:
|
|
344
|
+
children: /* @__PURE__ */ jsx(Text, { style: { color: "#03110c", fontWeight: "700", textAlign: "center" }, children: submitLabel })
|
|
273
345
|
}
|
|
274
346
|
)
|
|
275
347
|
] }) : null
|
package/dist/native/index.js.map
CHANGED
|
@@ -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"]}
|
package/dist/react/index.cjs
CHANGED
|
@@ -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." }) :
|
|
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
|
|
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
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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" :
|
|
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:
|
|
781
|
+
children: submitLabel
|
|
643
782
|
}
|
|
644
783
|
)
|
|
645
784
|
] })
|