@verbumia/feedback 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRACT.md +45 -2
- package/dist/{chunk-OX4RJD5H.js → chunk-NPUPBKOW.js} +2 -2
- package/dist/{chunk-OX4RJD5H.js.map → chunk-NPUPBKOW.js.map} +1 -1
- package/dist/core/index.cjs +1 -1
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/native/index.cjs +6 -4
- package/dist/native/index.cjs.map +1 -1
- package/dist/native/index.js +6 -4
- package/dist/native/index.js.map +1 -1
- package/dist/react/index.cjs +6 -4
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.js +6 -4
- package/dist/react/index.js.map +1 -1
- package/dist/svelte/index.cjs +6 -4
- package/dist/svelte/index.cjs.map +1 -1
- package/dist/svelte/index.js +6 -4
- package/dist/svelte/index.js.map +1 -1
- package/dist/vue/index.cjs +7 -5
- package/dist/vue/index.cjs.map +1 -1
- package/dist/vue/index.js +7 -5
- package/dist/vue/index.js.map +1 -1
- package/package.json +2 -2
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 } from \"../core/types\";\nimport { FeedbackModal } from \"./panel\";\n\nexport interface I18nPluginContext {\n config: {\n apiBase?: string;\n projectUuid: string;\n defaultLocale: string;\n };\n}\nexport interface I18nPlugin {\n name: string;\n setup?: (ctx: I18nPluginContext) => void | (() => void);\n render?: () => ReactNode;\n}\n\nexport interface FeedbackController {\n open: () => void;\n close: () => void;\n client: FeedbackClient;\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 onReady?: (controller: FeedbackController) => void;\n controllerRef?: { current: FeedbackController | null };\n fetchImpl?: typeof fetch;\n}\n\nfunction makeStore() {\n let open = false;\n const listeners = new Set<() => void>();\n return {\n isOpen: () => open,\n set(v: boolean) {\n if (open !== v) {\n open = v;\n listeners.forEach((l) => l());\n }\n },\n subscribe(l: () => void) {\n listeners.add(l);\n return () => listeners.delete(l);\n },\n };\n}\n\nexport function feedbackPlugin(options: FeedbackPluginOptions): I18nPlugin {\n const store = makeStore();\n let client: FeedbackClient | null = null;\n\n function Outlet() {\n const isOpen = useSyncExternalStore(\n store.subscribe,\n store.isOpen,\n store.isOpen,\n );\n if (!client) return null;\n const c = client;\n return (\n <FeedbackModal\n client={c}\n visible={isOpen}\n keys={options.keys}\n onClose={() => {\n store.set(false);\n void c.flush();\n }}\n />\n );\n }\n\n return {\n name: \"@verbumia/feedback\",\n setup(ctx) {\n client = new FeedbackClient({\n apiBase:\n options.apiBase ?? ctx.config.apiBase ?? \"https://api.verbumia.ca\",\n projectId: options.projectId ?? ctx.config.projectUuid,\n language: options.language ?? ctx.config.defaultLocale,\n endUserId: options.endUserId,\n fetchImpl: options.fetchImpl,\n });\n const controller: FeedbackController = {\n open: () => store.set(true),\n close: () => {\n store.set(false);\n void client?.flush();\n },\n client,\n };\n options.onReady?.(controller);\n if (options.controllerRef) options.controllerRef.current = controller;\n return () => {\n if (options.controllerRef) options.controllerRef.current = null;\n void client?.flush();\n };\n },\n render: () => <Outlet />,\n };\n}\n","import { useCallback, useEffect, useState } from \"react\";\nimport {\n Modal,\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\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 onClose: () => void;\n}) {\n const { client, visible, keys, onClose } = props;\n const [consented, setConsented] = useState(client.hasConsented);\n const [busy, setBusy] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [strings, setStrings] = useState<FeedbackString[]>([]);\n\n const loadStrings = useCallback(async () => {\n setBusy(true);\n setError(null);\n try {\n const resolved = resolveKeys(keys);\n const res = await client.getStrings({\n keys: resolved.length ? resolved : undefined,\n });\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]);\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 <View style={{ flex: 1, backgroundColor: \"rgba(0,0,0,.55)\", justifyContent: \"flex-end\" }}>\n <View\n style={{\n maxHeight: \"85%\",\n backgroundColor: C.bg,\n borderTopWidth: 1,\n borderColor: C.border,\n padding: 18,\n }}\n >\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 <ScrollView>\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 </View>\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 } from \"../core/types\";\nimport { FeedbackModal } from \"./panel\";\n\nexport interface I18nPluginContext {\n config: {\n apiBase?: string;\n projectUuid: string;\n defaultLocale: string;\n };\n}\nexport interface I18nPlugin {\n name: string;\n setup?: (ctx: I18nPluginContext) => void | (() => void);\n render?: () => ReactNode;\n}\n\nexport interface FeedbackController {\n open: () => void;\n close: () => void;\n client: FeedbackClient;\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 onReady?: (controller: FeedbackController) => void;\n controllerRef?: { current: FeedbackController | null };\n fetchImpl?: typeof fetch;\n}\n\nfunction makeStore() {\n let open = false;\n const listeners = new Set<() => void>();\n return {\n isOpen: () => open,\n set(v: boolean) {\n if (open !== v) {\n open = v;\n listeners.forEach((l) => l());\n }\n },\n subscribe(l: () => void) {\n listeners.add(l);\n return () => listeners.delete(l);\n },\n };\n}\n\nexport function feedbackPlugin(options: FeedbackPluginOptions): I18nPlugin {\n const store = makeStore();\n let client: FeedbackClient | null = null;\n\n function Outlet() {\n const isOpen = useSyncExternalStore(\n store.subscribe,\n store.isOpen,\n store.isOpen,\n );\n if (!client) return null;\n const c = client;\n return (\n <FeedbackModal\n client={c}\n visible={isOpen}\n keys={options.keys}\n onClose={() => {\n store.set(false);\n void c.flush();\n }}\n />\n );\n }\n\n return {\n name: \"@verbumia/feedback\",\n setup(ctx) {\n client = new FeedbackClient({\n apiBase:\n options.apiBase ?? ctx.config.apiBase ?? \"https://api.verbumia.ca\",\n projectId: options.projectId ?? ctx.config.projectUuid,\n language: options.language ?? ctx.config.defaultLocale,\n endUserId: options.endUserId,\n fetchImpl: options.fetchImpl,\n });\n const controller: FeedbackController = {\n open: () => store.set(true),\n close: () => {\n store.set(false);\n void client?.flush();\n },\n client,\n };\n options.onReady?.(controller);\n if (options.controllerRef) options.controllerRef.current = controller;\n return () => {\n if (options.controllerRef) options.controllerRef.current = null;\n void client?.flush();\n };\n },\n render: () => <Outlet />,\n };\n}\n","import { useCallback, useEffect, useState } from \"react\";\nimport {\n Modal,\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\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 onClose: () => void;\n}) {\n const { client, visible, keys, onClose } = props;\n const [consented, setConsented] = useState(client.hasConsented);\n const [busy, setBusy] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [strings, setStrings] = useState<FeedbackString[]>([]);\n\n const loadStrings = useCallback(async () => {\n setBusy(true);\n setError(null);\n try {\n // On-screen scoping is NORMATIVE (spec ltm 373, CONTRACT v5): show\n // ONLY rendered keys. Empty -> \"no strings on this view\"; never\n // fall back to fetching the whole project.\n const resolved = resolveKeys(keys);\n if (!resolved.length) {\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]);\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 <View style={{ flex: 1, backgroundColor: \"rgba(0,0,0,.55)\", justifyContent: \"flex-end\" }}>\n <View\n style={{\n maxHeight: \"85%\",\n backgroundColor: C.bg,\n borderTopWidth: 1,\n borderColor: C.border,\n padding: 18,\n }}\n >\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 <ScrollView>\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 </View>\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"]}
|
package/dist/react/index.cjs
CHANGED
|
@@ -50,7 +50,7 @@ var SDK_TOS_VERSION = "2026-05-18";
|
|
|
50
50
|
|
|
51
51
|
// src/core/client.ts
|
|
52
52
|
var SDK_LIB = "@verbumia/feedback";
|
|
53
|
-
var SDK_VER = "0.1.
|
|
53
|
+
var SDK_VER = "0.1.1";
|
|
54
54
|
var FeedbackClient = class {
|
|
55
55
|
cfg;
|
|
56
56
|
fetchImpl;
|
|
@@ -292,9 +292,11 @@ function FeedbackPanel(props) {
|
|
|
292
292
|
setError(null);
|
|
293
293
|
try {
|
|
294
294
|
const resolved = resolveKeys(keys);
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
295
|
+
if (!resolved.length) {
|
|
296
|
+
setStrings([]);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const res = await client.getStrings({ keys: resolved });
|
|
298
300
|
setStrings(res.strings);
|
|
299
301
|
} catch (e) {
|
|
300
302
|
setError(e instanceof Error ? e.message : "Failed to load strings");
|
package/dist/react/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/react/index.ts","../../src/react/plugin.tsx","../../src/core/types.ts","../../src/core/tos.ts","../../src/core/client.ts","../../src/react/panel.tsx","../../src/core/keys.ts"],"sourcesContent":["export {\n feedbackPlugin,\n type FeedbackController,\n type FeedbackPluginOptions,\n type I18nPlugin,\n type I18nPluginContext,\n} from \"./plugin\";\nexport { FeedbackPanel } from \"./panel\";\nexport {\n FeedbackClient,\n hasKeyRegistry,\n resolveKeys,\n type BatchResponse,\n type DeclaredKey,\n type FeedbackConfig,\n FeedbackError,\n type FeedbackString,\n type RatingInput,\n type StringsResponse,\n type SuggestionInput,\n type TokenBundle,\n} from \"../core\";\n","/**\n * `@verbumia/feedback` as a PLUGIN of the `@verbumia/*-i18n` provider.\n *\n * Architecture (task 599): NO second React context. The customer adds\n * `feedbackPlugin(...)` to the i18n provider's `plugins` slot. The\n * provider calls `setup({ i18n, config })` once (we reuse its apiBase /\n * projectUuid / locale — no re-config) and renders our `render()` as an\n * ISOLATED sibling leaf. The panel's open/close lives in a tiny private\n * store the outlet subscribes to via useSyncExternalStore, so toggling\n * feedback NEVER re-renders the host app. The customer triggers it via\n * their own CTA through the imperative controller.\n */\nimport { createPortal } from \"react-dom\";\nimport { useSyncExternalStore, type ReactNode } from \"react\";\n\nimport { FeedbackClient } from \"../core/client\";\nimport type { DeclaredKey } from \"../core/types\";\nimport { FeedbackPanel } from \"./panel\";\n\n/** Structural mirror of `@verbumia/react-i18next`'s VerbumiaPlugin —\n * kept local so feedback has no build-time dep on the i18n SDK. */\nexport interface I18nPluginContext {\n config: {\n apiBase?: string;\n projectUuid: string;\n defaultLocale: string;\n };\n}\nexport interface I18nPlugin {\n name: string;\n setup?: (ctx: I18nPluginContext) => void | (() => void);\n render?: () => ReactNode;\n}\n\nexport interface FeedbackController {\n open: () => void;\n close: () => void;\n /** The underlying client (advanced; usually unused). */\n client: FeedbackClient;\n}\n\nexport interface FeedbackPluginOptions {\n // NOTE: no `tosVersion` — it is an SDK build-time constant\n // (SDK_TOS_VERSION, task 616), sent automatically; never integrator-set.\n /** Override language; defaults to the i18n provider's defaultLocale. */\n language?: string;\n /** Override apiBase; defaults to the i18n provider's apiBase. */\n apiBase?: string;\n /** Override projectId; defaults to the i18n provider's projectUuid. */\n projectId?: string;\n /** Optional opaque end-user id (server generates one when absent). */\n endUserId?: string;\n /** Explicit on-screen keys (else the i18n key registry, if present). */\n keys?: DeclaredKey[];\n /** Receives the imperative { open, close } handle for the host CTA. */\n onReady?: (controller: FeedbackController) => void;\n /** Alt delivery: a ref object that receives the controller. */\n controllerRef?: { current: FeedbackController | null };\n /** Injected fetch (tests / RN polyfills). */\n fetchImpl?: typeof fetch;\n}\n\nfunction makeStore() {\n let open = false;\n const listeners = new Set<() => void>();\n return {\n isOpen: () => open,\n set(v: boolean) {\n if (open !== v) {\n open = v;\n listeners.forEach((l) => l());\n }\n },\n subscribe(l: () => void) {\n listeners.add(l);\n return () => listeners.delete(l);\n },\n };\n}\n\nexport function feedbackPlugin(options: FeedbackPluginOptions): I18nPlugin {\n const store = makeStore();\n let client: FeedbackClient | null = null;\n\n function Outlet() {\n const isOpen = useSyncExternalStore(\n store.subscribe,\n store.isOpen,\n store.isOpen,\n );\n if (!isOpen || !client || typeof document === \"undefined\") return null;\n const c = client;\n return createPortal(\n <FeedbackPanel\n client={c}\n keys={options.keys}\n onClose={() => {\n store.set(false);\n void c.flush();\n }}\n />,\n document.body,\n );\n }\n\n return {\n name: \"@verbumia/feedback\",\n setup(ctx) {\n client = new FeedbackClient({\n apiBase:\n options.apiBase ?? ctx.config.apiBase ?? \"https://api.verbumia.ca\",\n projectId: options.projectId ?? ctx.config.projectUuid,\n language: options.language ?? ctx.config.defaultLocale,\n endUserId: options.endUserId,\n fetchImpl: options.fetchImpl,\n });\n const controller: FeedbackController = {\n open: () => store.set(true),\n close: () => {\n store.set(false);\n void client?.flush();\n },\n client,\n };\n options.onReady?.(controller);\n if (options.controllerRef) options.controllerRef.current = controller;\n return () => {\n if (options.controllerRef) options.controllerRef.current = null;\n void client?.flush();\n };\n },\n render: () => <Outlet />,\n };\n}\n","/**\n * Wire types for the Verbumia End-User Translation Evaluation API.\n * Canonical reference: ./CONTRACT.md (frozen). Backend task 591.\n */\n\nexport interface FeedbackConfig {\n /** API base, no trailing slash needed. e.g. https://api.verbumia.ca */\n apiBase: string;\n /** Public project UUID the widget targets. */\n projectId: string;\n /**\n * sessionId / grouping_key is MINTED SERVER-SIDE by the Verbumia\n * backend at POST /v1/feedback/tos and returned in the token bundle\n * (`TokenBundle.grouping_key`). The widget receives + sends it; it\n * MUST NOT self-generate it. There is intentionally no client config\n * field for it.\n */\n /**\n * NOTE: there is intentionally NO `tosVersion` field. The ToS version\n * is a BUILD-TIME constant baked into @verbumia/feedback at release\n * (`SDK_TOS_VERSION`, task 616) — integrators do not set it; the SDK\n * sends it automatically.\n */\n /** Optional opaque end-user id; server generates one when absent. */\n endUserId?: string;\n /** BCP-47 language the widget rates strings in (e.g. \"fr\"). */\n language: string;\n /** Optional locale tag stored for analytics. */\n locale?: string;\n /** Debounce window (ms) before a queued batch is flushed. Default 1500. */\n flushDebounceMs?: number;\n /** Max queued items before an immediate flush. Default 20. */\n maxBatch?: number;\n /** Injected fetch (tests / RN polyfills). Defaults to global fetch. */\n fetchImpl?: typeof fetch;\n}\n\nexport interface TokenBundle {\n access_token: string;\n token_type: \"Bearer\";\n expires_in: number;\n refresh_token: string;\n refresh_expires_in: number;\n end_user_id: string;\n tos_version: string;\n grouping_key: string;\n}\n\nexport interface FeedbackString {\n namespace: string;\n key: string;\n key_uuid: string;\n language_uuid: string;\n value: string;\n translation_hash: string;\n avg_stars: number | null;\n ratings_count: number;\n my_rating: number | null;\n}\n\nexport interface StringsResponse {\n project_id: string;\n language: string;\n strings: FeedbackString[];\n}\n\n/** A 5-star rating queued for a rendered string variant. */\nexport interface RatingInput {\n namespace: string;\n key: string;\n language: string;\n translation_hash: string;\n stars: number; // 1..5\n}\n\n/** A suggested alternative translation queued for moderation. */\nexport interface SuggestionInput {\n namespace: string;\n key: string;\n language: string;\n translation_hash: string;\n suggested_text: string;\n comment?: string;\n}\n\nexport interface BatchResponse {\n accepted: number;\n rejected: number;\n items: Array<Record<string, unknown>>;\n}\n\n/** A key the host app declares as on-screen. */\nexport interface DeclaredKey {\n namespace: string;\n key: string;\n}\n\nexport class FeedbackError extends Error {\n constructor(\n message: string,\n readonly status?: number,\n readonly code?: string,\n ) {\n super(message);\n this.name = \"FeedbackError\";\n }\n}\n","/**\n * BUILD-TIME ToS version constant (task 616, human decision option B).\n *\n * This is baked into the @verbumia/feedback PACKAGE at release. It is\n * NOT an integrator-set config field and NOT server-driven: a stale\n * (e.g. native) app must record consent to the ToS version IT shipped\n * and displayed, for legal correctness — never backend-latest.\n *\n * Releasing a ToS-TEXT change ⇒ bump this constant + cut a new\n * @verbumia/feedback release (per the human/real-CI publish handoff).\n * The backend accepts any version in its acceptable set and records it.\n */\nexport const SDK_TOS_VERSION = \"2026-05-18\";\n","/**\n * Framework-agnostic Verbumia feedback client.\n *\n * Responsibilities:\n * - versioned-ToS acceptance -> scoped end-user JWT bootstrap\n * - transparent access-token refresh (rotating refresh token)\n * - fetch on-screen strings for the widget\n * - debounced + size-capped batched POST of ratings / suggestions,\n * mirroring the missing-handler transport contract (ltm 280):\n * best-effort, never throws into the host app render path.\n *\n * The React and React Native entry points are thin UI shells over this.\n */\n\nimport {\n type BatchResponse,\n type FeedbackConfig,\n FeedbackError,\n type RatingInput,\n type StringsResponse,\n type SuggestionInput,\n type TokenBundle,\n} from \"./types\";\nimport { SDK_TOS_VERSION } from \"./tos\";\n\nconst SDK_LIB = \"@verbumia/feedback\";\nconst SDK_VER = \"0.1.0\";\n\ntype QueueItem =\n | { kind: \"rating\"; payload: RatingInput }\n | { kind: \"suggestion\"; payload: SuggestionInput };\n\nexport class FeedbackClient {\n private cfg: Required<\n Pick<FeedbackConfig, \"flushDebounceMs\" | \"maxBatch\">\n > &\n FeedbackConfig;\n private fetchImpl: typeof fetch;\n private tokens: TokenBundle | null = null;\n private queue: QueueItem[] = [];\n private timer: ReturnType<typeof setTimeout> | null = null;\n private bootstrapping: Promise<TokenBundle> | null = null;\n\n constructor(config: FeedbackConfig) {\n this.cfg = {\n flushDebounceMs: 1500,\n maxBatch: 20,\n ...config,\n };\n const f = config.fetchImpl ?? globalThis.fetch;\n if (!f) {\n throw new FeedbackError(\n \"no fetch implementation available; pass config.fetchImpl\",\n );\n }\n this.fetchImpl = f.bind(globalThis);\n }\n\n private base(): string {\n return this.cfg.apiBase.replace(/\\/+$/, \"\");\n }\n\n get endUserId(): string | undefined {\n return this.tokens?.end_user_id ?? this.cfg.endUserId;\n }\n\n get hasConsented(): boolean {\n return this.tokens !== null;\n }\n\n /** Server-minted sessionId / grouping_key (from the token bundle).\n * Available only after `acceptTos()`; never client-generated. */\n get sessionId(): string | undefined {\n return this.tokens?.grouping_key;\n }\n\n /** BCP-47 language the widget is rating strings in. */\n get language(): string {\n return this.cfg.language;\n }\n\n /** ToS version the end user is asked to accept — the SDK's\n * build-time constant (task 616). NOT integrator/server set. */\n get tosVersion(): string {\n return SDK_TOS_VERSION;\n }\n\n /**\n * Accept the ToS and bootstrap a scoped token. Idempotent: a second call\n * returns the in-flight / existing bundle rather than re-accepting.\n */\n async acceptTos(): Promise<TokenBundle> {\n if (this.tokens) return this.tokens;\n if (this.bootstrapping) return this.bootstrapping;\n this.bootstrapping = (async () => {\n const res = await this.fetchImpl(`${this.base()}/v1/feedback/tos`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n // NO grouping_key: server-minted (task 599). It comes back in\n // the token bundle and is bound into the JWT server-side.\n project_id: this.cfg.projectId,\n end_user_id: this.cfg.endUserId,\n tos_version: SDK_TOS_VERSION,\n locale: this.cfg.locale,\n }),\n });\n if (!res.ok) throw await this.problem(res, \"tos acceptance failed\");\n this.tokens = (await res.json()) as TokenBundle;\n return this.tokens;\n })();\n try {\n return await this.bootstrapping;\n } finally {\n this.bootstrapping = null;\n }\n }\n\n private async refresh(): Promise<void> {\n if (!this.tokens) throw new FeedbackError(\"not consented\");\n const res = await this.fetchImpl(\n `${this.base()}/v1/feedback/token/refresh`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ refresh_token: this.tokens.refresh_token }),\n },\n );\n if (!res.ok) {\n this.tokens = null; // force a fresh ToS step\n throw await this.problem(res, \"token refresh failed\");\n }\n this.tokens = (await res.json()) as TokenBundle;\n }\n\n /** Authenticated fetch with a single transparent refresh-on-401 retry. */\n private async authed(\n path: string,\n init: RequestInit,\n retry = true,\n ): Promise<Response> {\n if (!this.tokens) await this.acceptTos();\n const res = await this.fetchImpl(`${this.base()}${path}`, {\n ...init,\n headers: {\n ...(init.headers ?? {}),\n Authorization: `Bearer ${this.tokens!.access_token}`,\n },\n });\n if (res.status === 401 && retry) {\n await this.refresh();\n return this.authed(path, init, false);\n }\n return res;\n }\n\n /** Strings rendered on the current view, with this end user's prior rating. */\n async getStrings(opts?: {\n keys?: Array<{ namespace: string; key: string }>;\n namespace?: string;\n limit?: number;\n }): Promise<StringsResponse> {\n const qs = new URLSearchParams({ language: this.cfg.language });\n if (opts?.keys?.length) {\n qs.set(\"keys\", opts.keys.map((k) => `${k.namespace}:${k.key}`).join(\",\"));\n }\n if (opts?.namespace) qs.set(\"namespace\", opts.namespace);\n if (opts?.limit) qs.set(\"limit\", String(opts.limit));\n const res = await this.authed(`/v1/feedback/strings?${qs}`, {\n method: \"GET\",\n });\n if (!res.ok) throw await this.problem(res, \"failed to load strings\");\n return (await res.json()) as StringsResponse;\n }\n\n /** Queue a rating; flushed on debounce or when the batch fills. */\n rate(payload: RatingInput): void {\n this.enqueue({ kind: \"rating\", payload });\n }\n\n /** Queue a suggestion; flushed on debounce or when the batch fills. */\n suggest(payload: SuggestionInput): void {\n this.enqueue({ kind: \"suggestion\", payload });\n }\n\n private enqueue(item: QueueItem): void {\n this.queue.push(item);\n if (this.queue.length >= this.cfg.maxBatch) {\n void this.flush();\n return;\n }\n if (this.timer) clearTimeout(this.timer);\n this.timer = setTimeout(() => void this.flush(), this.cfg.flushDebounceMs);\n }\n\n /**\n * Flush queued items. Best-effort: a transport/auth failure re-queues the\n * batch once and swallows the error so the host app never sees a throw.\n */\n async flush(): Promise<void> {\n if (this.timer) {\n clearTimeout(this.timer);\n this.timer = null;\n }\n if (!this.queue.length) return;\n const batch = this.queue;\n this.queue = [];\n const ratings = batch\n .filter((b) => b.kind === \"rating\")\n .map((b) => b.payload as RatingInput);\n const suggestions = batch\n .filter((b) => b.kind === \"suggestion\")\n .map((b) => b.payload as SuggestionInput);\n try {\n if (ratings.length) {\n await this.postBatch(\"/v1/feedback/ratings\", { ratings });\n }\n if (suggestions.length) {\n await this.postBatch(\"/v1/feedback/suggestions\", { suggestions });\n }\n } catch {\n // Re-queue once; feedback must never break the host app.\n this.queue.unshift(...batch);\n }\n }\n\n private async postBatch(\n path: string,\n body: unknown,\n ): Promise<BatchResponse> {\n const res = await this.authed(path, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", \"X-SDK\": `${SDK_LIB}@${SDK_VER}` },\n body: JSON.stringify(body),\n });\n if (!res.ok) throw await this.problem(res, \"batch post failed\");\n return (await res.json()) as BatchResponse;\n }\n\n private async problem(res: Response, fallback: string): Promise<FeedbackError> {\n let code: string | undefined;\n let detail = fallback;\n try {\n const body = (await res.json()) as { code?: string; detail?: string };\n code = body.code;\n if (body.detail) detail = body.detail;\n } catch {\n /* non-JSON body */\n }\n return new FeedbackError(detail, res.status, code);\n }\n}\n","import { useCallback, useEffect, useState } from \"react\";\n\nimport type { FeedbackClient } from \"../core/client\";\nimport { resolveKeys } from \"../core/keys\";\nimport type { DeclaredKey, FeedbackString } from \"../core/types\";\n\nconst C = {\n bg: \"#0b0f0e\",\n panel: \"#111714\",\n border: \"#1f2a25\",\n text: \"#e7f5ef\",\n dim: \"#8aa79b\",\n emerald: \"#10b981\",\n emeraldSoft: \"#34d399\",\n};\n\nexport function FeedbackPanel(props: {\n client: FeedbackClient;\n keys?: DeclaredKey[];\n onClose: () => void;\n}) {\n const { client, keys, onClose } = props;\n const [consented, setConsented] = useState(client.hasConsented);\n const [busy, setBusy] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [strings, setStrings] = useState<FeedbackString[]>([]);\n\n const loadStrings = useCallback(async () => {\n setBusy(true);\n setError(null);\n try {\n const resolved = resolveKeys(keys);\n const res = await client.getStrings({\n keys: resolved.length ? resolved : undefined,\n });\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]);\n\n useEffect(() => {\n if (consented) void loadStrings();\n }, [consented, loadStrings]);\n\n const accept = useCallback(async () => {\n setBusy(true);\n setError(null);\n try {\n await client.acceptTos();\n setConsented(true);\n } catch (e) {\n setError(e instanceof Error ? e.message : \"Could not accept the terms\");\n } finally {\n setBusy(false);\n }\n }, [client]);\n\n return (\n <div\n role=\"dialog\"\n aria-label=\"Translation feedback\"\n style={{\n position: \"fixed\",\n inset: 0,\n zIndex: 2147483600,\n background: \"rgba(0,0,0,.55)\",\n display: \"flex\",\n justifyContent: \"flex-end\",\n }}\n onClick={onClose}\n >\n <div\n onClick={(e) => e.stopPropagation()}\n style={{\n width: \"min(420px, 100%)\",\n height: \"100%\",\n background: C.bg,\n color: C.text,\n borderLeft: `1px solid ${C.border}`,\n display: \"flex\",\n flexDirection: \"column\",\n fontFamily: \"system-ui, sans-serif\",\n }}\n >\n <header\n style={{\n padding: \"16px 18px\",\n borderBottom: `1px solid ${C.border}`,\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n }}\n >\n <strong style={{ color: C.emeraldSoft }}>Translation feedback</strong>\n <button\n type=\"button\"\n onClick={onClose}\n aria-label=\"Close\"\n style={{\n background: \"transparent\",\n color: C.dim,\n border: \"none\",\n fontSize: 20,\n cursor: \"pointer\",\n }}\n >\n ×\n </button>\n </header>\n\n <div style={{ padding: 18, overflowY: \"auto\", flex: 1 }}>\n {error && (\n <p style={{ color: \"#f87171\", fontSize: 13 }}>{error}</p>\n )}\n\n {!consented ? (\n <ConsentStep\n version={client.tosVersion}\n busy={busy}\n onAccept={accept}\n />\n ) : busy && !strings.length ? (\n <p style={{ color: C.dim }}>Loading…</p>\n ) : !strings.length ? (\n <p style={{ color: C.dim }}>No strings to review on this view.</p>\n ) : (\n strings.map((s) => (\n <StringRow key={`${s.namespace}:${s.key}`} s={s} client={client} />\n ))\n )}\n </div>\n <footer\n style={{\n padding: \"10px 18px\",\n borderTop: `1px solid ${C.border}`,\n color: C.dim,\n fontSize: 11,\n }}\n >\n Powered by Verbumia\n </footer>\n </div>\n </div>\n );\n}\n\nfunction ConsentStep(props: {\n version?: string;\n busy: boolean;\n onAccept: () => void;\n}) {\n return (\n <div>\n <p style={{ lineHeight: 1.5, fontSize: 14 }}>\n Help improve the translations in this app. Your ratings and\n suggestions are sent to the app owner via Verbumia. Don’t submit\n personal or sensitive information.\n </p>\n <button\n type=\"button\"\n disabled={props.busy}\n onClick={props.onAccept}\n style={{\n marginTop: 12,\n width: \"100%\",\n background: C.emerald,\n color: \"#03110c\",\n border: \"none\",\n borderRadius: 8,\n padding: \"12px 14px\",\n fontWeight: 700,\n cursor: props.busy ? \"default\" : \"pointer\",\n opacity: props.busy ? 0.6 : 1,\n }}\n >\n {props.busy\n ? \"…\"\n : `I accept the terms${props.version ? ` (v${props.version})` : \"\"}`}\n </button>\n </div>\n );\n}\n\nfunction StringRow(props: { s: FeedbackString; client: FeedbackClient }) {\n const { s, client } = props;\n const [mine, setMine] = useState<number | null>(s.my_rating);\n const [showSuggest, setShowSuggest] = useState(false);\n const [text, setText] = useState(\"\");\n const [sent, setSent] = useState(false);\n\n const rate = (stars: number) => {\n setMine(stars);\n client.rate({\n namespace: s.namespace,\n key: s.key,\n language: client.language,\n translation_hash: s.translation_hash,\n stars,\n });\n };\n\n const submitSuggestion = () => {\n if (!text.trim()) return;\n client.suggest({\n namespace: s.namespace,\n key: s.key,\n language: client.language,\n translation_hash: s.translation_hash,\n suggested_text: text.trim(),\n });\n setSent(true);\n setShowSuggest(false);\n setText(\"\");\n };\n\n return (\n <div\n style={{\n background: C.panel,\n border: `1px solid ${C.border}`,\n borderRadius: 10,\n padding: 12,\n marginBottom: 10,\n }}\n >\n <div style={{ fontSize: 12, color: C.dim }}>\n {s.namespace} · {s.key}\n </div>\n <div style={{ margin: \"6px 0 10px\", fontSize: 15 }}>{s.value}</div>\n <div style={{ display: \"flex\", gap: 4 }}>\n {[1, 2, 3, 4, 5].map((n) => (\n <button\n key={n}\n type=\"button\"\n aria-label={`${n} star${n > 1 ? \"s\" : \"\"}`}\n onClick={() => rate(n)}\n style={{\n background: \"transparent\",\n border: \"none\",\n cursor: \"pointer\",\n fontSize: 20,\n color: mine && n <= mine ? C.emeraldSoft : C.border,\n }}\n >\n ★\n </button>\n ))}\n <button\n type=\"button\"\n onClick={() => setShowSuggest((v) => !v)}\n style={{\n marginLeft: \"auto\",\n background: \"transparent\",\n color: C.emeraldSoft,\n border: `1px solid ${C.border}`,\n borderRadius: 6,\n padding: \"4px 10px\",\n fontSize: 12,\n cursor: \"pointer\",\n }}\n >\n {sent ? \"Suggested ✓\" : \"Suggest\"}\n </button>\n </div>\n {showSuggest && (\n <div style={{ marginTop: 10 }}>\n <textarea\n value={text}\n onChange={(e) => setText(e.target.value)}\n rows={3}\n placeholder=\"Your suggested translation…\"\n style={{\n width: \"100%\",\n background: C.bg,\n color: C.text,\n border: `1px solid ${C.border}`,\n borderRadius: 6,\n padding: 8,\n fontSize: 14,\n resize: \"vertical\",\n }}\n />\n <button\n type=\"button\"\n onClick={submitSuggestion}\n style={{\n marginTop: 6,\n background: C.emerald,\n color: \"#03110c\",\n border: \"none\",\n borderRadius: 6,\n padding: \"8px 12px\",\n fontWeight: 700,\n cursor: \"pointer\",\n }}\n >\n Send suggestion\n </button>\n </div>\n )}\n </div>\n );\n}\n","/**\n * On-screen key discovery.\n *\n * Preferred source: the `@verbumia/*-i18n` SDK exposes a lightweight key\n * registry of keys it has rendered. When that registry is present on the\n * global (the i18n SDK publishes `globalThis.__verbumia_key_registry__`),\n * we read the keys touched on the current view. Otherwise the host app\n * passes an explicit `keys` list — the always-available fallback.\n *\n * The registry contract is intentionally tiny so any framework port of the\n * i18n SDK can implement it without depending on this package.\n */\n\nimport type { DeclaredKey } from \"./types\";\n\nconst REGISTRY_GLOBAL = \"__verbumia_key_registry__\";\n\ninterface KeyRegistry {\n /** Returns the keys rendered since the last `reset()` (or ever). */\n snapshot(): DeclaredKey[];\n reset?(): void;\n}\n\nfunction getRegistry(): KeyRegistry | null {\n const g = globalThis as Record<string, unknown>;\n const reg = g[REGISTRY_GLOBAL];\n if (\n reg &&\n typeof (reg as KeyRegistry).snapshot === \"function\"\n ) {\n return reg as KeyRegistry;\n }\n return null;\n}\n\n/** True when a compatible `@verbumia/*-i18n` registry is detectable. */\nexport function hasKeyRegistry(): boolean {\n return getRegistry() !== null;\n}\n\n/**\n * Resolve the on-screen keys: explicit `keys` prop always wins (it is the\n * customer's authoritative declaration); otherwise fall back to the i18n\n * registry snapshot; otherwise an empty list (widget shows \"no strings\").\n */\nexport function resolveKeys(explicit?: DeclaredKey[]): DeclaredKey[] {\n if (explicit && explicit.length) return dedupe(explicit);\n const reg = getRegistry();\n if (reg) return dedupe(reg.snapshot());\n return [];\n}\n\nfunction dedupe(keys: DeclaredKey[]): DeclaredKey[] {\n const seen = new Set<string>();\n const out: DeclaredKey[] = [];\n for (const k of keys) {\n const id = `${k.namespace}:${k.key}`;\n if (!seen.has(id)) {\n seen.add(id);\n out.push(k);\n }\n }\n return out;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACYA,uBAA6B;AAC7B,IAAAA,gBAAqD;;;ACoF9C,IAAM,gBAAN,cAA4B,MAAM;AAAA,EACvC,YACE,SACS,QACA,MACT;AACA,UAAM,OAAO;AAHJ;AACA;AAGT,SAAK,OAAO;AAAA,EACd;AAAA,EALW;AAAA,EACA;AAKb;;;AC9FO,IAAM,kBAAkB;;;ACa/B,IAAM,UAAU;AAChB,IAAM,UAAU;AAMT,IAAM,iBAAN,MAAqB;AAAA,EAClB;AAAA,EAIA;AAAA,EACA,SAA6B;AAAA,EAC7B,QAAqB,CAAC;AAAA,EACtB,QAA8C;AAAA,EAC9C,gBAA6C;AAAA,EAErD,YAAY,QAAwB;AAClC,SAAK,MAAM;AAAA,MACT,iBAAiB;AAAA,MACjB,UAAU;AAAA,MACV,GAAG;AAAA,IACL;AACA,UAAM,IAAI,OAAO,aAAa,WAAW;AACzC,QAAI,CAAC,GAAG;AACN,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,SAAK,YAAY,EAAE,KAAK,UAAU;AAAA,EACpC;AAAA,EAEQ,OAAe;AACrB,WAAO,KAAK,IAAI,QAAQ,QAAQ,QAAQ,EAAE;AAAA,EAC5C;AAAA,EAEA,IAAI,YAAgC;AAClC,WAAO,KAAK,QAAQ,eAAe,KAAK,IAAI;AAAA,EAC9C;AAAA,EAEA,IAAI,eAAwB;AAC1B,WAAO,KAAK,WAAW;AAAA,EACzB;AAAA;AAAA;AAAA,EAIA,IAAI,YAAgC;AAClC,WAAO,KAAK,QAAQ;AAAA,EACtB;AAAA;AAAA,EAGA,IAAI,WAAmB;AACrB,WAAO,KAAK,IAAI;AAAA,EAClB;AAAA;AAAA;AAAA,EAIA,IAAI,aAAqB;AACvB,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,YAAkC;AACtC,QAAI,KAAK,OAAQ,QAAO,KAAK;AAC7B,QAAI,KAAK,cAAe,QAAO,KAAK;AACpC,SAAK,iBAAiB,YAAY;AAChC,YAAM,MAAM,MAAM,KAAK,UAAU,GAAG,KAAK,KAAK,CAAC,oBAAoB;AAAA,QACjE,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU;AAAA;AAAA;AAAA,UAGnB,YAAY,KAAK,IAAI;AAAA,UACrB,aAAa,KAAK,IAAI;AAAA,UACtB,aAAa;AAAA,UACb,QAAQ,KAAK,IAAI;AAAA,QACnB,CAAC;AAAA,MACH,CAAC;AACD,UAAI,CAAC,IAAI,GAAI,OAAM,MAAM,KAAK,QAAQ,KAAK,uBAAuB;AAClE,WAAK,SAAU,MAAM,IAAI,KAAK;AAC9B,aAAO,KAAK;AAAA,IACd,GAAG;AACH,QAAI;AACF,aAAO,MAAM,KAAK;AAAA,IACpB,UAAE;AACA,WAAK,gBAAgB;AAAA,IACvB;AAAA,EACF;AAAA,EAEA,MAAc,UAAyB;AACrC,QAAI,CAAC,KAAK,OAAQ,OAAM,IAAI,cAAc,eAAe;AACzD,UAAM,MAAM,MAAM,KAAK;AAAA,MACrB,GAAG,KAAK,KAAK,CAAC;AAAA,MACd;AAAA,QACE,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU,EAAE,eAAe,KAAK,OAAO,cAAc,CAAC;AAAA,MACnE;AAAA,IACF;AACA,QAAI,CAAC,IAAI,IAAI;AACX,WAAK,SAAS;AACd,YAAM,MAAM,KAAK,QAAQ,KAAK,sBAAsB;AAAA,IACtD;AACA,SAAK,SAAU,MAAM,IAAI,KAAK;AAAA,EAChC;AAAA;AAAA,EAGA,MAAc,OACZ,MACA,MACA,QAAQ,MACW;AACnB,QAAI,CAAC,KAAK,OAAQ,OAAM,KAAK,UAAU;AACvC,UAAM,MAAM,MAAM,KAAK,UAAU,GAAG,KAAK,KAAK,CAAC,GAAG,IAAI,IAAI;AAAA,MACxD,GAAG;AAAA,MACH,SAAS;AAAA,QACP,GAAI,KAAK,WAAW,CAAC;AAAA,QACrB,eAAe,UAAU,KAAK,OAAQ,YAAY;AAAA,MACpD;AAAA,IACF,CAAC;AACD,QAAI,IAAI,WAAW,OAAO,OAAO;AAC/B,YAAM,KAAK,QAAQ;AACnB,aAAO,KAAK,OAAO,MAAM,MAAM,KAAK;AAAA,IACtC;AACA,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,MAAM,WAAW,MAIY;AAC3B,UAAM,KAAK,IAAI,gBAAgB,EAAE,UAAU,KAAK,IAAI,SAAS,CAAC;AAC9D,QAAI,MAAM,MAAM,QAAQ;AACtB,SAAG,IAAI,QAAQ,KAAK,KAAK,IAAI,CAAC,MAAM,GAAG,EAAE,SAAS,IAAI,EAAE,GAAG,EAAE,EAAE,KAAK,GAAG,CAAC;AAAA,IAC1E;AACA,QAAI,MAAM,UAAW,IAAG,IAAI,aAAa,KAAK,SAAS;AACvD,QAAI,MAAM,MAAO,IAAG,IAAI,SAAS,OAAO,KAAK,KAAK,CAAC;AACnD,UAAM,MAAM,MAAM,KAAK,OAAO,wBAAwB,EAAE,IAAI;AAAA,MAC1D,QAAQ;AAAA,IACV,CAAC;AACD,QAAI,CAAC,IAAI,GAAI,OAAM,MAAM,KAAK,QAAQ,KAAK,wBAAwB;AACnE,WAAQ,MAAM,IAAI,KAAK;AAAA,EACzB;AAAA;AAAA,EAGA,KAAK,SAA4B;AAC/B,SAAK,QAAQ,EAAE,MAAM,UAAU,QAAQ,CAAC;AAAA,EAC1C;AAAA;AAAA,EAGA,QAAQ,SAAgC;AACtC,SAAK,QAAQ,EAAE,MAAM,cAAc,QAAQ,CAAC;AAAA,EAC9C;AAAA,EAEQ,QAAQ,MAAuB;AACrC,SAAK,MAAM,KAAK,IAAI;AACpB,QAAI,KAAK,MAAM,UAAU,KAAK,IAAI,UAAU;AAC1C,WAAK,KAAK,MAAM;AAChB;AAAA,IACF;AACA,QAAI,KAAK,MAAO,cAAa,KAAK,KAAK;AACvC,SAAK,QAAQ,WAAW,MAAM,KAAK,KAAK,MAAM,GAAG,KAAK,IAAI,eAAe;AAAA,EAC3E;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,QAAuB;AAC3B,QAAI,KAAK,OAAO;AACd,mBAAa,KAAK,KAAK;AACvB,WAAK,QAAQ;AAAA,IACf;AACA,QAAI,CAAC,KAAK,MAAM,OAAQ;AACxB,UAAM,QAAQ,KAAK;AACnB,SAAK,QAAQ,CAAC;AACd,UAAM,UAAU,MACb,OAAO,CAAC,MAAM,EAAE,SAAS,QAAQ,EACjC,IAAI,CAAC,MAAM,EAAE,OAAsB;AACtC,UAAM,cAAc,MACjB,OAAO,CAAC,MAAM,EAAE,SAAS,YAAY,EACrC,IAAI,CAAC,MAAM,EAAE,OAA0B;AAC1C,QAAI;AACF,UAAI,QAAQ,QAAQ;AAClB,cAAM,KAAK,UAAU,wBAAwB,EAAE,QAAQ,CAAC;AAAA,MAC1D;AACA,UAAI,YAAY,QAAQ;AACtB,cAAM,KAAK,UAAU,4BAA4B,EAAE,YAAY,CAAC;AAAA,MAClE;AAAA,IACF,QAAQ;AAEN,WAAK,MAAM,QAAQ,GAAG,KAAK;AAAA,IAC7B;AAAA,EACF;AAAA,EAEA,MAAc,UACZ,MACA,MACwB;AACxB,UAAM,MAAM,MAAM,KAAK,OAAO,MAAM;AAAA,MAClC,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,oBAAoB,SAAS,GAAG,OAAO,IAAI,OAAO,GAAG;AAAA,MAChF,MAAM,KAAK,UAAU,IAAI;AAAA,IAC3B,CAAC;AACD,QAAI,CAAC,IAAI,GAAI,OAAM,MAAM,KAAK,QAAQ,KAAK,mBAAmB;AAC9D,WAAQ,MAAM,IAAI,KAAK;AAAA,EACzB;AAAA,EAEA,MAAc,QAAQ,KAAe,UAA0C;AAC7E,QAAI;AACJ,QAAI,SAAS;AACb,QAAI;AACF,YAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,aAAO,KAAK;AACZ,UAAI,KAAK,OAAQ,UAAS,KAAK;AAAA,IACjC,QAAQ;AAAA,IAER;AACA,WAAO,IAAI,cAAc,QAAQ,IAAI,QAAQ,IAAI;AAAA,EACnD;AACF;;;AC3PA,mBAAiD;;;ACejD,IAAM,kBAAkB;AAQxB,SAAS,cAAkC;AACzC,QAAM,IAAI;AACV,QAAM,MAAM,EAAE,eAAe;AAC7B,MACE,OACA,OAAQ,IAAoB,aAAa,YACzC;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAGO,SAAS,iBAA0B;AACxC,SAAO,YAAY,MAAM;AAC3B;AAOO,SAAS,YAAY,UAAyC;AACnE,MAAI,YAAY,SAAS,OAAQ,QAAO,OAAO,QAAQ;AACvD,QAAM,MAAM,YAAY;AACxB,MAAI,IAAK,QAAO,OAAO,IAAI,SAAS,CAAC;AACrC,SAAO,CAAC;AACV;AAEA,SAAS,OAAO,MAAoC;AAClD,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,MAAqB,CAAC;AAC5B,aAAW,KAAK,MAAM;AACpB,UAAM,KAAK,GAAG,EAAE,SAAS,IAAI,EAAE,GAAG;AAClC,QAAI,CAAC,KAAK,IAAI,EAAE,GAAG;AACjB,WAAK,IAAI,EAAE;AACX,UAAI,KAAK,CAAC;AAAA,IACZ;AAAA,EACF;AACA,SAAO;AACT;;;ADwBQ;AAjFR,IAAM,IAAI;AAAA,EACR,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,KAAK;AAAA,EACL,SAAS;AAAA,EACT,aAAa;AACf;AAEO,SAAS,cAAc,OAI3B;AACD,QAAM,EAAE,QAAQ,MAAM,QAAQ,IAAI;AAClC,QAAM,CAAC,WAAW,YAAY,QAAI,uBAAS,OAAO,YAAY;AAC9D,QAAM,CAAC,MAAM,OAAO,QAAI,uBAAS,KAAK;AACtC,QAAM,CAAC,OAAO,QAAQ,QAAI,uBAAwB,IAAI;AACtD,QAAM,CAAC,SAAS,UAAU,QAAI,uBAA2B,CAAC,CAAC;AAE3D,QAAM,kBAAc,0BAAY,YAAY;AAC1C,YAAQ,IAAI;AACZ,aAAS,IAAI;AACb,QAAI;AACF,YAAM,WAAW,YAAY,IAAI;AACjC,YAAM,MAAM,MAAM,OAAO,WAAW;AAAA,QAClC,MAAM,SAAS,SAAS,WAAW;AAAA,MACrC,CAAC;AACD,iBAAW,IAAI,OAAO;AAAA,IACxB,SAAS,GAAG;AACV,eAAS,aAAa,QAAQ,EAAE,UAAU,wBAAwB;AAAA,IACpE,UAAE;AACA,cAAQ,KAAK;AAAA,IACf;AAAA,EACF,GAAG,CAAC,QAAQ,IAAI,CAAC;AAEjB,8BAAU,MAAM;AACd,QAAI,UAAW,MAAK,YAAY;AAAA,EAClC,GAAG,CAAC,WAAW,WAAW,CAAC;AAE3B,QAAM,aAAS,0BAAY,YAAY;AACrC,YAAQ,IAAI;AACZ,aAAS,IAAI;AACb,QAAI;AACF,YAAM,OAAO,UAAU;AACvB,mBAAa,IAAI;AAAA,IACnB,SAAS,GAAG;AACV,eAAS,aAAa,QAAQ,EAAE,UAAU,4BAA4B;AAAA,IACxE,UAAE;AACA,cAAQ,KAAK;AAAA,IACf;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAEX,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAW;AAAA,MACX,OAAO;AAAA,QACL,UAAU;AAAA,QACV,OAAO;AAAA,QACP,QAAQ;AAAA,QACR,YAAY;AAAA,QACZ,SAAS;AAAA,QACT,gBAAgB;AAAA,MAClB;AAAA,MACA,SAAS;AAAA,MAET;AAAA,QAAC;AAAA;AAAA,UACC,SAAS,CAAC,MAAM,EAAE,gBAAgB;AAAA,UAClC,OAAO;AAAA,YACL,OAAO;AAAA,YACP,QAAQ;AAAA,YACR,YAAY,EAAE;AAAA,YACd,OAAO,EAAE;AAAA,YACT,YAAY,aAAa,EAAE,MAAM;AAAA,YACjC,SAAS;AAAA,YACT,eAAe;AAAA,YACf,YAAY;AAAA,UACd;AAAA,UAEA;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,OAAO;AAAA,kBACL,SAAS;AAAA,kBACT,cAAc,aAAa,EAAE,MAAM;AAAA,kBACnC,SAAS;AAAA,kBACT,gBAAgB;AAAA,kBAChB,YAAY;AAAA,gBACd;AAAA,gBAEA;AAAA,8DAAC,YAAO,OAAO,EAAE,OAAO,EAAE,YAAY,GAAG,kCAAoB;AAAA,kBAC7D;AAAA,oBAAC;AAAA;AAAA,sBACC,MAAK;AAAA,sBACL,SAAS;AAAA,sBACT,cAAW;AAAA,sBACX,OAAO;AAAA,wBACL,YAAY;AAAA,wBACZ,OAAO,EAAE;AAAA,wBACT,QAAQ;AAAA,wBACR,UAAU;AAAA,wBACV,QAAQ;AAAA,sBACV;AAAA,sBACD;AAAA;AAAA,kBAED;AAAA;AAAA;AAAA,YACF;AAAA,YAEA,6CAAC,SAAI,OAAO,EAAE,SAAS,IAAI,WAAW,QAAQ,MAAM,EAAE,GACnD;AAAA,uBACC,4CAAC,OAAE,OAAO,EAAE,OAAO,WAAW,UAAU,GAAG,GAAI,iBAAM;AAAA,cAGtD,CAAC,YACA;AAAA,gBAAC;AAAA;AAAA,kBACC,SAAS,OAAO;AAAA,kBAChB;AAAA,kBACA,UAAU;AAAA;AAAA,cACZ,IACE,QAAQ,CAAC,QAAQ,SACnB,4CAAC,OAAE,OAAO,EAAE,OAAO,EAAE,IAAI,GAAG,2BAAQ,IAClC,CAAC,QAAQ,SACX,4CAAC,OAAE,OAAO,EAAE,OAAO,EAAE,IAAI,GAAG,gDAAkC,IAE9D,QAAQ,IAAI,CAAC,MACX,4CAAC,aAA0C,GAAM,UAAjC,GAAG,EAAE,SAAS,IAAI,EAAE,GAAG,EAA0B,CAClE;AAAA,eAEL;AAAA,YACA;AAAA,cAAC;AAAA;AAAA,gBACC,OAAO;AAAA,kBACL,SAAS;AAAA,kBACT,WAAW,aAAa,EAAE,MAAM;AAAA,kBAChC,OAAO,EAAE;AAAA,kBACT,UAAU;AAAA,gBACZ;AAAA,gBACD;AAAA;AAAA,YAED;AAAA;AAAA;AAAA,MACF;AAAA;AAAA,EACF;AAEJ;AAEA,SAAS,YAAY,OAIlB;AACD,SACE,6CAAC,SACC;AAAA,gDAAC,OAAE,OAAO,EAAE,YAAY,KAAK,UAAU,GAAG,GAAG,kLAI7C;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,UAAU,MAAM;AAAA,QAChB,SAAS,MAAM;AAAA,QACf,OAAO;AAAA,UACL,WAAW;AAAA,UACX,OAAO;AAAA,UACP,YAAY,EAAE;AAAA,UACd,OAAO;AAAA,UACP,QAAQ;AAAA,UACR,cAAc;AAAA,UACd,SAAS;AAAA,UACT,YAAY;AAAA,UACZ,QAAQ,MAAM,OAAO,YAAY;AAAA,UACjC,SAAS,MAAM,OAAO,MAAM;AAAA,QAC9B;AAAA,QAEC,gBAAM,OACH,WACA,qBAAqB,MAAM,UAAU,MAAM,MAAM,OAAO,MAAM,EAAE;AAAA;AAAA,IACtE;AAAA,KACF;AAEJ;AAEA,SAAS,UAAU,OAAsD;AACvE,QAAM,EAAE,GAAG,OAAO,IAAI;AACtB,QAAM,CAAC,MAAM,OAAO,QAAI,uBAAwB,EAAE,SAAS;AAC3D,QAAM,CAAC,aAAa,cAAc,QAAI,uBAAS,KAAK;AACpD,QAAM,CAAC,MAAM,OAAO,QAAI,uBAAS,EAAE;AACnC,QAAM,CAAC,MAAM,OAAO,QAAI,uBAAS,KAAK;AAEtC,QAAM,OAAO,CAAC,UAAkB;AAC9B,YAAQ,KAAK;AACb,WAAO,KAAK;AAAA,MACV,WAAW,EAAE;AAAA,MACb,KAAK,EAAE;AAAA,MACP,UAAU,OAAO;AAAA,MACjB,kBAAkB,EAAE;AAAA,MACpB;AAAA,IACF,CAAC;AAAA,EACH;AAEA,QAAM,mBAAmB,MAAM;AAC7B,QAAI,CAAC,KAAK,KAAK,EAAG;AAClB,WAAO,QAAQ;AAAA,MACb,WAAW,EAAE;AAAA,MACb,KAAK,EAAE;AAAA,MACP,UAAU,OAAO;AAAA,MACjB,kBAAkB,EAAE;AAAA,MACpB,gBAAgB,KAAK,KAAK;AAAA,IAC5B,CAAC;AACD,YAAQ,IAAI;AACZ,mBAAe,KAAK;AACpB,YAAQ,EAAE;AAAA,EACZ;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAO;AAAA,QACL,YAAY,EAAE;AAAA,QACd,QAAQ,aAAa,EAAE,MAAM;AAAA,QAC7B,cAAc;AAAA,QACd,SAAS;AAAA,QACT,cAAc;AAAA,MAChB;AAAA,MAEA;AAAA,qDAAC,SAAI,OAAO,EAAE,UAAU,IAAI,OAAO,EAAE,IAAI,GACtC;AAAA,YAAE;AAAA,UAAU;AAAA,UAAI,EAAE;AAAA,WACrB;AAAA,QACA,4CAAC,SAAI,OAAO,EAAE,QAAQ,cAAc,UAAU,GAAG,GAAI,YAAE,OAAM;AAAA,QAC7D,6CAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,EAAE,GACnC;AAAA,WAAC,GAAG,GAAG,GAAG,GAAG,CAAC,EAAE,IAAI,CAAC,MACpB;AAAA,YAAC;AAAA;AAAA,cAEC,MAAK;AAAA,cACL,cAAY,GAAG,CAAC,QAAQ,IAAI,IAAI,MAAM,EAAE;AAAA,cACxC,SAAS,MAAM,KAAK,CAAC;AAAA,cACrB,OAAO;AAAA,gBACL,YAAY;AAAA,gBACZ,QAAQ;AAAA,gBACR,QAAQ;AAAA,gBACR,UAAU;AAAA,gBACV,OAAO,QAAQ,KAAK,OAAO,EAAE,cAAc,EAAE;AAAA,cAC/C;AAAA,cACD;AAAA;AAAA,YAXM;AAAA,UAaP,CACD;AAAA,UACD;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAS,MAAM,eAAe,CAAC,MAAM,CAAC,CAAC;AAAA,cACvC,OAAO;AAAA,gBACL,YAAY;AAAA,gBACZ,YAAY;AAAA,gBACZ,OAAO,EAAE;AAAA,gBACT,QAAQ,aAAa,EAAE,MAAM;AAAA,gBAC7B,cAAc;AAAA,gBACd,SAAS;AAAA,gBACT,UAAU;AAAA,gBACV,QAAQ;AAAA,cACV;AAAA,cAEC,iBAAO,qBAAgB;AAAA;AAAA,UAC1B;AAAA,WACF;AAAA,QACC,eACC,6CAAC,SAAI,OAAO,EAAE,WAAW,GAAG,GAC1B;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP,UAAU,CAAC,MAAM,QAAQ,EAAE,OAAO,KAAK;AAAA,cACvC,MAAM;AAAA,cACN,aAAY;AAAA,cACZ,OAAO;AAAA,gBACL,OAAO;AAAA,gBACP,YAAY,EAAE;AAAA,gBACd,OAAO,EAAE;AAAA,gBACT,QAAQ,aAAa,EAAE,MAAM;AAAA,gBAC7B,cAAc;AAAA,gBACd,SAAS;AAAA,gBACT,UAAU;AAAA,gBACV,QAAQ;AAAA,cACV;AAAA;AAAA,UACF;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAS;AAAA,cACT,OAAO;AAAA,gBACL,WAAW;AAAA,gBACX,YAAY,EAAE;AAAA,gBACd,OAAO;AAAA,gBACP,QAAQ;AAAA,gBACR,cAAc;AAAA,gBACd,SAAS;AAAA,gBACT,YAAY;AAAA,gBACZ,QAAQ;AAAA,cACV;AAAA,cACD;AAAA;AAAA,UAED;AAAA,WACF;AAAA;AAAA;AAAA,EAEJ;AAEJ;;;AJpNM,IAAAC,sBAAA;AA/BN,SAAS,YAAY;AACnB,MAAI,OAAO;AACX,QAAM,YAAY,oBAAI,IAAgB;AACtC,SAAO;AAAA,IACL,QAAQ,MAAM;AAAA,IACd,IAAI,GAAY;AACd,UAAI,SAAS,GAAG;AACd,eAAO;AACP,kBAAU,QAAQ,CAAC,MAAM,EAAE,CAAC;AAAA,MAC9B;AAAA,IACF;AAAA,IACA,UAAU,GAAe;AACvB,gBAAU,IAAI,CAAC;AACf,aAAO,MAAM,UAAU,OAAO,CAAC;AAAA,IACjC;AAAA,EACF;AACF;AAEO,SAAS,eAAe,SAA4C;AACzE,QAAM,QAAQ,UAAU;AACxB,MAAI,SAAgC;AAEpC,WAAS,SAAS;AAChB,UAAM,aAAS;AAAA,MACb,MAAM;AAAA,MACN,MAAM;AAAA,MACN,MAAM;AAAA,IACR;AACA,QAAI,CAAC,UAAU,CAAC,UAAU,OAAO,aAAa,YAAa,QAAO;AAClE,UAAM,IAAI;AACV,eAAO;AAAA,MACL;AAAA,QAAC;AAAA;AAAA,UACC,QAAQ;AAAA,UACR,MAAM,QAAQ;AAAA,UACd,SAAS,MAAM;AACb,kBAAM,IAAI,KAAK;AACf,iBAAK,EAAE,MAAM;AAAA,UACf;AAAA;AAAA,MACF;AAAA,MACA,SAAS;AAAA,IACX;AAAA,EACF;AAEA,SAAO;AAAA,IACL,MAAM;AAAA,IACN,MAAM,KAAK;AACT,eAAS,IAAI,eAAe;AAAA,QAC1B,SACE,QAAQ,WAAW,IAAI,OAAO,WAAW;AAAA,QAC3C,WAAW,QAAQ,aAAa,IAAI,OAAO;AAAA,QAC3C,UAAU,QAAQ,YAAY,IAAI,OAAO;AAAA,QACzC,WAAW,QAAQ;AAAA,QACnB,WAAW,QAAQ;AAAA,MACrB,CAAC;AACD,YAAM,aAAiC;AAAA,QACrC,MAAM,MAAM,MAAM,IAAI,IAAI;AAAA,QAC1B,OAAO,MAAM;AACX,gBAAM,IAAI,KAAK;AACf,eAAK,QAAQ,MAAM;AAAA,QACrB;AAAA,QACA;AAAA,MACF;AACA,cAAQ,UAAU,UAAU;AAC5B,UAAI,QAAQ,cAAe,SAAQ,cAAc,UAAU;AAC3D,aAAO,MAAM;AACX,YAAI,QAAQ,cAAe,SAAQ,cAAc,UAAU;AAC3D,aAAK,QAAQ,MAAM;AAAA,MACrB;AAAA,IACF;AAAA,IACA,QAAQ,MAAM,6CAAC,UAAO;AAAA,EACxB;AACF;","names":["import_react","import_jsx_runtime"]}
|
|
1
|
+
{"version":3,"sources":["../../src/react/index.ts","../../src/react/plugin.tsx","../../src/core/types.ts","../../src/core/tos.ts","../../src/core/client.ts","../../src/react/panel.tsx","../../src/core/keys.ts"],"sourcesContent":["export {\n feedbackPlugin,\n type FeedbackController,\n type FeedbackPluginOptions,\n type I18nPlugin,\n type I18nPluginContext,\n} from \"./plugin\";\nexport { FeedbackPanel } from \"./panel\";\nexport {\n FeedbackClient,\n hasKeyRegistry,\n resolveKeys,\n type BatchResponse,\n type DeclaredKey,\n type FeedbackConfig,\n FeedbackError,\n type FeedbackString,\n type RatingInput,\n type StringsResponse,\n type SuggestionInput,\n type TokenBundle,\n} from \"../core\";\n","/**\n * `@verbumia/feedback` as a PLUGIN of the `@verbumia/*-i18n` provider.\n *\n * Architecture (task 599): NO second React context. The customer adds\n * `feedbackPlugin(...)` to the i18n provider's `plugins` slot. The\n * provider calls `setup({ i18n, config })` once (we reuse its apiBase /\n * projectUuid / locale — no re-config) and renders our `render()` as an\n * ISOLATED sibling leaf. The panel's open/close lives in a tiny private\n * store the outlet subscribes to via useSyncExternalStore, so toggling\n * feedback NEVER re-renders the host app. The customer triggers it via\n * their own CTA through the imperative controller.\n */\nimport { createPortal } from \"react-dom\";\nimport { useSyncExternalStore, type ReactNode } from \"react\";\n\nimport { FeedbackClient } from \"../core/client\";\nimport type { DeclaredKey } from \"../core/types\";\nimport { FeedbackPanel } from \"./panel\";\n\n/** Structural mirror of `@verbumia/react-i18next`'s VerbumiaPlugin —\n * kept local so feedback has no build-time dep on the i18n SDK. */\nexport interface I18nPluginContext {\n config: {\n apiBase?: string;\n projectUuid: string;\n defaultLocale: string;\n };\n}\nexport interface I18nPlugin {\n name: string;\n setup?: (ctx: I18nPluginContext) => void | (() => void);\n render?: () => ReactNode;\n}\n\nexport interface FeedbackController {\n open: () => void;\n close: () => void;\n /** The underlying client (advanced; usually unused). */\n client: FeedbackClient;\n}\n\nexport interface FeedbackPluginOptions {\n // NOTE: no `tosVersion` — it is an SDK build-time constant\n // (SDK_TOS_VERSION, task 616), sent automatically; never integrator-set.\n /** Override language; defaults to the i18n provider's defaultLocale. */\n language?: string;\n /** Override apiBase; defaults to the i18n provider's apiBase. */\n apiBase?: string;\n /** Override projectId; defaults to the i18n provider's projectUuid. */\n projectId?: string;\n /** Optional opaque end-user id (server generates one when absent). */\n endUserId?: string;\n /** Explicit on-screen keys (else the i18n key registry, if present). */\n keys?: DeclaredKey[];\n /** Receives the imperative { open, close } handle for the host CTA. */\n onReady?: (controller: FeedbackController) => void;\n /** Alt delivery: a ref object that receives the controller. */\n controllerRef?: { current: FeedbackController | null };\n /** Injected fetch (tests / RN polyfills). */\n fetchImpl?: typeof fetch;\n}\n\nfunction makeStore() {\n let open = false;\n const listeners = new Set<() => void>();\n return {\n isOpen: () => open,\n set(v: boolean) {\n if (open !== v) {\n open = v;\n listeners.forEach((l) => l());\n }\n },\n subscribe(l: () => void) {\n listeners.add(l);\n return () => listeners.delete(l);\n },\n };\n}\n\nexport function feedbackPlugin(options: FeedbackPluginOptions): I18nPlugin {\n const store = makeStore();\n let client: FeedbackClient | null = null;\n\n function Outlet() {\n const isOpen = useSyncExternalStore(\n store.subscribe,\n store.isOpen,\n store.isOpen,\n );\n if (!isOpen || !client || typeof document === \"undefined\") return null;\n const c = client;\n return createPortal(\n <FeedbackPanel\n client={c}\n keys={options.keys}\n onClose={() => {\n store.set(false);\n void c.flush();\n }}\n />,\n document.body,\n );\n }\n\n return {\n name: \"@verbumia/feedback\",\n setup(ctx) {\n client = new FeedbackClient({\n apiBase:\n options.apiBase ?? ctx.config.apiBase ?? \"https://api.verbumia.ca\",\n projectId: options.projectId ?? ctx.config.projectUuid,\n language: options.language ?? ctx.config.defaultLocale,\n endUserId: options.endUserId,\n fetchImpl: options.fetchImpl,\n });\n const controller: FeedbackController = {\n open: () => store.set(true),\n close: () => {\n store.set(false);\n void client?.flush();\n },\n client,\n };\n options.onReady?.(controller);\n if (options.controllerRef) options.controllerRef.current = controller;\n return () => {\n if (options.controllerRef) options.controllerRef.current = null;\n void client?.flush();\n };\n },\n render: () => <Outlet />,\n };\n}\n","/**\n * Wire types for the Verbumia End-User Translation Evaluation API.\n * Canonical reference: ./CONTRACT.md (frozen). Backend task 591.\n */\n\nexport interface FeedbackConfig {\n /** API base, no trailing slash needed. e.g. https://api.verbumia.ca */\n apiBase: string;\n /** Public project UUID the widget targets. */\n projectId: string;\n /**\n * sessionId / grouping_key is MINTED SERVER-SIDE by the Verbumia\n * backend at POST /v1/feedback/tos and returned in the token bundle\n * (`TokenBundle.grouping_key`). The widget receives + sends it; it\n * MUST NOT self-generate it. There is intentionally no client config\n * field for it.\n */\n /**\n * NOTE: there is intentionally NO `tosVersion` field. The ToS version\n * is a BUILD-TIME constant baked into @verbumia/feedback at release\n * (`SDK_TOS_VERSION`, task 616) — integrators do not set it; the SDK\n * sends it automatically.\n */\n /** Optional opaque end-user id; server generates one when absent. */\n endUserId?: string;\n /** BCP-47 language the widget rates strings in (e.g. \"fr\"). */\n language: string;\n /** Optional locale tag stored for analytics. */\n locale?: string;\n /** Debounce window (ms) before a queued batch is flushed. Default 1500. */\n flushDebounceMs?: number;\n /** Max queued items before an immediate flush. Default 20. */\n maxBatch?: number;\n /** Injected fetch (tests / RN polyfills). Defaults to global fetch. */\n fetchImpl?: typeof fetch;\n}\n\nexport interface TokenBundle {\n access_token: string;\n token_type: \"Bearer\";\n expires_in: number;\n refresh_token: string;\n refresh_expires_in: number;\n end_user_id: string;\n tos_version: string;\n grouping_key: string;\n}\n\nexport interface FeedbackString {\n namespace: string;\n key: string;\n key_uuid: string;\n language_uuid: string;\n value: string;\n translation_hash: string;\n avg_stars: number | null;\n ratings_count: number;\n my_rating: number | null;\n}\n\nexport interface StringsResponse {\n project_id: string;\n language: string;\n strings: FeedbackString[];\n}\n\n/** A 5-star rating queued for a rendered string variant. */\nexport interface RatingInput {\n namespace: string;\n key: string;\n language: string;\n translation_hash: string;\n stars: number; // 1..5\n}\n\n/** A suggested alternative translation queued for moderation. */\nexport interface SuggestionInput {\n namespace: string;\n key: string;\n language: string;\n translation_hash: string;\n suggested_text: string;\n comment?: string;\n}\n\nexport interface BatchResponse {\n accepted: number;\n rejected: number;\n items: Array<Record<string, unknown>>;\n}\n\n/** A key the host app declares as on-screen. */\nexport interface DeclaredKey {\n namespace: string;\n key: string;\n}\n\nexport class FeedbackError extends Error {\n constructor(\n message: string,\n readonly status?: number,\n readonly code?: string,\n ) {\n super(message);\n this.name = \"FeedbackError\";\n }\n}\n","/**\n * BUILD-TIME ToS version constant (task 616, human decision option B).\n *\n * This is baked into the @verbumia/feedback PACKAGE at release. It is\n * NOT an integrator-set config field and NOT server-driven: a stale\n * (e.g. native) app must record consent to the ToS version IT shipped\n * and displayed, for legal correctness — never backend-latest.\n *\n * Releasing a ToS-TEXT change ⇒ bump this constant + cut a new\n * @verbumia/feedback release (per the human/real-CI publish handoff).\n * The backend accepts any version in its acceptable set and records it.\n */\nexport const SDK_TOS_VERSION = \"2026-05-18\";\n","/**\n * Framework-agnostic Verbumia feedback client.\n *\n * Responsibilities:\n * - versioned-ToS acceptance -> scoped end-user JWT bootstrap\n * - transparent access-token refresh (rotating refresh token)\n * - fetch on-screen strings for the widget\n * - debounced + size-capped batched POST of ratings / suggestions,\n * mirroring the missing-handler transport contract (ltm 280):\n * best-effort, never throws into the host app render path.\n *\n * The React and React Native entry points are thin UI shells over this.\n */\n\nimport {\n type BatchResponse,\n type FeedbackConfig,\n FeedbackError,\n type RatingInput,\n type StringsResponse,\n type SuggestionInput,\n type TokenBundle,\n} from \"./types\";\nimport { SDK_TOS_VERSION } from \"./tos\";\n\nconst SDK_LIB = \"@verbumia/feedback\";\nconst SDK_VER = \"0.1.1\";\n\ntype QueueItem =\n | { kind: \"rating\"; payload: RatingInput }\n | { kind: \"suggestion\"; payload: SuggestionInput };\n\nexport class FeedbackClient {\n private cfg: Required<\n Pick<FeedbackConfig, \"flushDebounceMs\" | \"maxBatch\">\n > &\n FeedbackConfig;\n private fetchImpl: typeof fetch;\n private tokens: TokenBundle | null = null;\n private queue: QueueItem[] = [];\n private timer: ReturnType<typeof setTimeout> | null = null;\n private bootstrapping: Promise<TokenBundle> | null = null;\n\n constructor(config: FeedbackConfig) {\n this.cfg = {\n flushDebounceMs: 1500,\n maxBatch: 20,\n ...config,\n };\n const f = config.fetchImpl ?? globalThis.fetch;\n if (!f) {\n throw new FeedbackError(\n \"no fetch implementation available; pass config.fetchImpl\",\n );\n }\n this.fetchImpl = f.bind(globalThis);\n }\n\n private base(): string {\n return this.cfg.apiBase.replace(/\\/+$/, \"\");\n }\n\n get endUserId(): string | undefined {\n return this.tokens?.end_user_id ?? this.cfg.endUserId;\n }\n\n get hasConsented(): boolean {\n return this.tokens !== null;\n }\n\n /** Server-minted sessionId / grouping_key (from the token bundle).\n * Available only after `acceptTos()`; never client-generated. */\n get sessionId(): string | undefined {\n return this.tokens?.grouping_key;\n }\n\n /** BCP-47 language the widget is rating strings in. */\n get language(): string {\n return this.cfg.language;\n }\n\n /** ToS version the end user is asked to accept — the SDK's\n * build-time constant (task 616). NOT integrator/server set. */\n get tosVersion(): string {\n return SDK_TOS_VERSION;\n }\n\n /**\n * Accept the ToS and bootstrap a scoped token. Idempotent: a second call\n * returns the in-flight / existing bundle rather than re-accepting.\n */\n async acceptTos(): Promise<TokenBundle> {\n if (this.tokens) return this.tokens;\n if (this.bootstrapping) return this.bootstrapping;\n this.bootstrapping = (async () => {\n const res = await this.fetchImpl(`${this.base()}/v1/feedback/tos`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n // NO grouping_key: server-minted (task 599). It comes back in\n // the token bundle and is bound into the JWT server-side.\n project_id: this.cfg.projectId,\n end_user_id: this.cfg.endUserId,\n tos_version: SDK_TOS_VERSION,\n locale: this.cfg.locale,\n }),\n });\n if (!res.ok) throw await this.problem(res, \"tos acceptance failed\");\n this.tokens = (await res.json()) as TokenBundle;\n return this.tokens;\n })();\n try {\n return await this.bootstrapping;\n } finally {\n this.bootstrapping = null;\n }\n }\n\n private async refresh(): Promise<void> {\n if (!this.tokens) throw new FeedbackError(\"not consented\");\n const res = await this.fetchImpl(\n `${this.base()}/v1/feedback/token/refresh`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ refresh_token: this.tokens.refresh_token }),\n },\n );\n if (!res.ok) {\n this.tokens = null; // force a fresh ToS step\n throw await this.problem(res, \"token refresh failed\");\n }\n this.tokens = (await res.json()) as TokenBundle;\n }\n\n /** Authenticated fetch with a single transparent refresh-on-401 retry. */\n private async authed(\n path: string,\n init: RequestInit,\n retry = true,\n ): Promise<Response> {\n if (!this.tokens) await this.acceptTos();\n const res = await this.fetchImpl(`${this.base()}${path}`, {\n ...init,\n headers: {\n ...(init.headers ?? {}),\n Authorization: `Bearer ${this.tokens!.access_token}`,\n },\n });\n if (res.status === 401 && retry) {\n await this.refresh();\n return this.authed(path, init, false);\n }\n return res;\n }\n\n /** Strings rendered on the current view, with this end user's prior rating. */\n async getStrings(opts?: {\n keys?: Array<{ namespace: string; key: string }>;\n namespace?: string;\n limit?: number;\n }): Promise<StringsResponse> {\n const qs = new URLSearchParams({ language: this.cfg.language });\n if (opts?.keys?.length) {\n qs.set(\"keys\", opts.keys.map((k) => `${k.namespace}:${k.key}`).join(\",\"));\n }\n if (opts?.namespace) qs.set(\"namespace\", opts.namespace);\n if (opts?.limit) qs.set(\"limit\", String(opts.limit));\n const res = await this.authed(`/v1/feedback/strings?${qs}`, {\n method: \"GET\",\n });\n if (!res.ok) throw await this.problem(res, \"failed to load strings\");\n return (await res.json()) as StringsResponse;\n }\n\n /** Queue a rating; flushed on debounce or when the batch fills. */\n rate(payload: RatingInput): void {\n this.enqueue({ kind: \"rating\", payload });\n }\n\n /** Queue a suggestion; flushed on debounce or when the batch fills. */\n suggest(payload: SuggestionInput): void {\n this.enqueue({ kind: \"suggestion\", payload });\n }\n\n private enqueue(item: QueueItem): void {\n this.queue.push(item);\n if (this.queue.length >= this.cfg.maxBatch) {\n void this.flush();\n return;\n }\n if (this.timer) clearTimeout(this.timer);\n this.timer = setTimeout(() => void this.flush(), this.cfg.flushDebounceMs);\n }\n\n /**\n * Flush queued items. Best-effort: a transport/auth failure re-queues the\n * batch once and swallows the error so the host app never sees a throw.\n */\n async flush(): Promise<void> {\n if (this.timer) {\n clearTimeout(this.timer);\n this.timer = null;\n }\n if (!this.queue.length) return;\n const batch = this.queue;\n this.queue = [];\n const ratings = batch\n .filter((b) => b.kind === \"rating\")\n .map((b) => b.payload as RatingInput);\n const suggestions = batch\n .filter((b) => b.kind === \"suggestion\")\n .map((b) => b.payload as SuggestionInput);\n try {\n if (ratings.length) {\n await this.postBatch(\"/v1/feedback/ratings\", { ratings });\n }\n if (suggestions.length) {\n await this.postBatch(\"/v1/feedback/suggestions\", { suggestions });\n }\n } catch {\n // Re-queue once; feedback must never break the host app.\n this.queue.unshift(...batch);\n }\n }\n\n private async postBatch(\n path: string,\n body: unknown,\n ): Promise<BatchResponse> {\n const res = await this.authed(path, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", \"X-SDK\": `${SDK_LIB}@${SDK_VER}` },\n body: JSON.stringify(body),\n });\n if (!res.ok) throw await this.problem(res, \"batch post failed\");\n return (await res.json()) as BatchResponse;\n }\n\n private async problem(res: Response, fallback: string): Promise<FeedbackError> {\n let code: string | undefined;\n let detail = fallback;\n try {\n const body = (await res.json()) as { code?: string; detail?: string };\n code = body.code;\n if (body.detail) detail = body.detail;\n } catch {\n /* non-JSON body */\n }\n return new FeedbackError(detail, res.status, code);\n }\n}\n","import { useCallback, useEffect, useState } from \"react\";\n\nimport type { FeedbackClient } from \"../core/client\";\nimport { resolveKeys } from \"../core/keys\";\nimport type { DeclaredKey, FeedbackString } from \"../core/types\";\n\nconst C = {\n bg: \"#0b0f0e\",\n panel: \"#111714\",\n border: \"#1f2a25\",\n text: \"#e7f5ef\",\n dim: \"#8aa79b\",\n emerald: \"#10b981\",\n emeraldSoft: \"#34d399\",\n};\n\nexport function FeedbackPanel(props: {\n client: FeedbackClient;\n keys?: DeclaredKey[];\n onClose: () => void;\n}) {\n const { client, keys, onClose } = props;\n const [consented, setConsented] = useState(client.hasConsented);\n const [busy, setBusy] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [strings, setStrings] = useState<FeedbackString[]>([]);\n\n const loadStrings = useCallback(async () => {\n setBusy(true);\n setError(null);\n try {\n // On-screen scoping is NORMATIVE (spec ltm 373, CONTRACT v5): show\n // ONLY rendered keys. Empty -> \"no strings on this view\"; never\n // fall back to fetching the whole project.\n const resolved = resolveKeys(keys);\n if (!resolved.length) {\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]);\n\n useEffect(() => {\n if (consented) void loadStrings();\n }, [consented, loadStrings]);\n\n const accept = useCallback(async () => {\n setBusy(true);\n setError(null);\n try {\n await client.acceptTos();\n setConsented(true);\n } catch (e) {\n setError(e instanceof Error ? e.message : \"Could not accept the terms\");\n } finally {\n setBusy(false);\n }\n }, [client]);\n\n return (\n <div\n role=\"dialog\"\n aria-label=\"Translation feedback\"\n style={{\n position: \"fixed\",\n inset: 0,\n zIndex: 2147483600,\n background: \"rgba(0,0,0,.55)\",\n display: \"flex\",\n justifyContent: \"flex-end\",\n }}\n onClick={onClose}\n >\n <div\n onClick={(e) => e.stopPropagation()}\n style={{\n width: \"min(420px, 100%)\",\n height: \"100%\",\n background: C.bg,\n color: C.text,\n borderLeft: `1px solid ${C.border}`,\n display: \"flex\",\n flexDirection: \"column\",\n fontFamily: \"system-ui, sans-serif\",\n }}\n >\n <header\n style={{\n padding: \"16px 18px\",\n borderBottom: `1px solid ${C.border}`,\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n }}\n >\n <strong style={{ color: C.emeraldSoft }}>Translation feedback</strong>\n <button\n type=\"button\"\n onClick={onClose}\n aria-label=\"Close\"\n style={{\n background: \"transparent\",\n color: C.dim,\n border: \"none\",\n fontSize: 20,\n cursor: \"pointer\",\n }}\n >\n ×\n </button>\n </header>\n\n <div style={{ padding: 18, overflowY: \"auto\", flex: 1 }}>\n {error && (\n <p style={{ color: \"#f87171\", fontSize: 13 }}>{error}</p>\n )}\n\n {!consented ? (\n <ConsentStep\n version={client.tosVersion}\n busy={busy}\n onAccept={accept}\n />\n ) : busy && !strings.length ? (\n <p style={{ color: C.dim }}>Loading…</p>\n ) : !strings.length ? (\n <p style={{ color: C.dim }}>No strings to review on this view.</p>\n ) : (\n strings.map((s) => (\n <StringRow key={`${s.namespace}:${s.key}`} s={s} client={client} />\n ))\n )}\n </div>\n <footer\n style={{\n padding: \"10px 18px\",\n borderTop: `1px solid ${C.border}`,\n color: C.dim,\n fontSize: 11,\n }}\n >\n Powered by Verbumia\n </footer>\n </div>\n </div>\n );\n}\n\nfunction ConsentStep(props: {\n version?: string;\n busy: boolean;\n onAccept: () => void;\n}) {\n return (\n <div>\n <p style={{ lineHeight: 1.5, fontSize: 14 }}>\n Help improve the translations in this app. Your ratings and\n suggestions are sent to the app owner via Verbumia. Don’t submit\n personal or sensitive information.\n </p>\n <button\n type=\"button\"\n disabled={props.busy}\n onClick={props.onAccept}\n style={{\n marginTop: 12,\n width: \"100%\",\n background: C.emerald,\n color: \"#03110c\",\n border: \"none\",\n borderRadius: 8,\n padding: \"12px 14px\",\n fontWeight: 700,\n cursor: props.busy ? \"default\" : \"pointer\",\n opacity: props.busy ? 0.6 : 1,\n }}\n >\n {props.busy\n ? \"…\"\n : `I accept the terms${props.version ? ` (v${props.version})` : \"\"}`}\n </button>\n </div>\n );\n}\n\nfunction StringRow(props: { s: FeedbackString; client: FeedbackClient }) {\n const { s, client } = props;\n const [mine, setMine] = useState<number | null>(s.my_rating);\n const [showSuggest, setShowSuggest] = useState(false);\n const [text, setText] = useState(\"\");\n const [sent, setSent] = useState(false);\n\n const rate = (stars: number) => {\n setMine(stars);\n client.rate({\n namespace: s.namespace,\n key: s.key,\n language: client.language,\n translation_hash: s.translation_hash,\n stars,\n });\n };\n\n const submitSuggestion = () => {\n if (!text.trim()) return;\n client.suggest({\n namespace: s.namespace,\n key: s.key,\n language: client.language,\n translation_hash: s.translation_hash,\n suggested_text: text.trim(),\n });\n setSent(true);\n setShowSuggest(false);\n setText(\"\");\n };\n\n return (\n <div\n style={{\n background: C.panel,\n border: `1px solid ${C.border}`,\n borderRadius: 10,\n padding: 12,\n marginBottom: 10,\n }}\n >\n <div style={{ fontSize: 12, color: C.dim }}>\n {s.namespace} · {s.key}\n </div>\n <div style={{ margin: \"6px 0 10px\", fontSize: 15 }}>{s.value}</div>\n <div style={{ display: \"flex\", gap: 4 }}>\n {[1, 2, 3, 4, 5].map((n) => (\n <button\n key={n}\n type=\"button\"\n aria-label={`${n} star${n > 1 ? \"s\" : \"\"}`}\n onClick={() => rate(n)}\n style={{\n background: \"transparent\",\n border: \"none\",\n cursor: \"pointer\",\n fontSize: 20,\n color: mine && n <= mine ? C.emeraldSoft : C.border,\n }}\n >\n ★\n </button>\n ))}\n <button\n type=\"button\"\n onClick={() => setShowSuggest((v) => !v)}\n style={{\n marginLeft: \"auto\",\n background: \"transparent\",\n color: C.emeraldSoft,\n border: `1px solid ${C.border}`,\n borderRadius: 6,\n padding: \"4px 10px\",\n fontSize: 12,\n cursor: \"pointer\",\n }}\n >\n {sent ? \"Suggested ✓\" : \"Suggest\"}\n </button>\n </div>\n {showSuggest && (\n <div style={{ marginTop: 10 }}>\n <textarea\n value={text}\n onChange={(e) => setText(e.target.value)}\n rows={3}\n placeholder=\"Your suggested translation…\"\n style={{\n width: \"100%\",\n background: C.bg,\n color: C.text,\n border: `1px solid ${C.border}`,\n borderRadius: 6,\n padding: 8,\n fontSize: 14,\n resize: \"vertical\",\n }}\n />\n <button\n type=\"button\"\n onClick={submitSuggestion}\n style={{\n marginTop: 6,\n background: C.emerald,\n color: \"#03110c\",\n border: \"none\",\n borderRadius: 6,\n padding: \"8px 12px\",\n fontWeight: 700,\n cursor: \"pointer\",\n }}\n >\n Send suggestion\n </button>\n </div>\n )}\n </div>\n );\n}\n","/**\n * On-screen key discovery.\n *\n * Preferred source: the `@verbumia/*-i18n` SDK exposes a lightweight key\n * registry of keys it has rendered. When that registry is present on the\n * global (the i18n SDK publishes `globalThis.__verbumia_key_registry__`),\n * we read the keys touched on the current view. Otherwise the host app\n * passes an explicit `keys` list — the always-available fallback.\n *\n * The registry contract is intentionally tiny so any framework port of the\n * i18n SDK can implement it without depending on this package.\n */\n\nimport type { DeclaredKey } from \"./types\";\n\nconst REGISTRY_GLOBAL = \"__verbumia_key_registry__\";\n\ninterface KeyRegistry {\n /** Returns the keys rendered since the last `reset()` (or ever). */\n snapshot(): DeclaredKey[];\n reset?(): void;\n}\n\nfunction getRegistry(): KeyRegistry | null {\n const g = globalThis as Record<string, unknown>;\n const reg = g[REGISTRY_GLOBAL];\n if (\n reg &&\n typeof (reg as KeyRegistry).snapshot === \"function\"\n ) {\n return reg as KeyRegistry;\n }\n return null;\n}\n\n/** True when a compatible `@verbumia/*-i18n` registry is detectable. */\nexport function hasKeyRegistry(): boolean {\n return getRegistry() !== null;\n}\n\n/**\n * Resolve the on-screen keys: explicit `keys` prop always wins (it is the\n * customer's authoritative declaration); otherwise fall back to the i18n\n * registry snapshot; otherwise an empty list (widget shows \"no strings\").\n */\nexport function resolveKeys(explicit?: DeclaredKey[]): DeclaredKey[] {\n if (explicit && explicit.length) return dedupe(explicit);\n const reg = getRegistry();\n if (reg) return dedupe(reg.snapshot());\n return [];\n}\n\nfunction dedupe(keys: DeclaredKey[]): DeclaredKey[] {\n const seen = new Set<string>();\n const out: DeclaredKey[] = [];\n for (const k of keys) {\n const id = `${k.namespace}:${k.key}`;\n if (!seen.has(id)) {\n seen.add(id);\n out.push(k);\n }\n }\n return out;\n}\n"],"mappings":"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","names":["import_react","import_jsx_runtime"]}
|
package/dist/react/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
FeedbackError,
|
|
5
5
|
hasKeyRegistry,
|
|
6
6
|
resolveKeys
|
|
7
|
-
} from "../chunk-
|
|
7
|
+
} from "../chunk-NPUPBKOW.js";
|
|
8
8
|
|
|
9
9
|
// src/react/plugin.tsx
|
|
10
10
|
import { createPortal } from "react-dom";
|
|
@@ -33,9 +33,11 @@ function FeedbackPanel(props) {
|
|
|
33
33
|
setError(null);
|
|
34
34
|
try {
|
|
35
35
|
const resolved = resolveKeys(keys);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
if (!resolved.length) {
|
|
37
|
+
setStrings([]);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const res = await client.getStrings({ keys: resolved });
|
|
39
41
|
setStrings(res.strings);
|
|
40
42
|
} catch (e) {
|
|
41
43
|
setError(e instanceof Error ? e.message : "Failed to load strings");
|
package/dist/react/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/react/plugin.tsx","../../src/react/panel.tsx"],"sourcesContent":["/**\n * `@verbumia/feedback` as a PLUGIN of the `@verbumia/*-i18n` provider.\n *\n * Architecture (task 599): NO second React context. The customer adds\n * `feedbackPlugin(...)` to the i18n provider's `plugins` slot. The\n * provider calls `setup({ i18n, config })` once (we reuse its apiBase /\n * projectUuid / locale — no re-config) and renders our `render()` as an\n * ISOLATED sibling leaf. The panel's open/close lives in a tiny private\n * store the outlet subscribes to via useSyncExternalStore, so toggling\n * feedback NEVER re-renders the host app. The customer triggers it via\n * their own CTA through the imperative controller.\n */\nimport { createPortal } from \"react-dom\";\nimport { useSyncExternalStore, type ReactNode } from \"react\";\n\nimport { FeedbackClient } from \"../core/client\";\nimport type { DeclaredKey } from \"../core/types\";\nimport { FeedbackPanel } from \"./panel\";\n\n/** Structural mirror of `@verbumia/react-i18next`'s VerbumiaPlugin —\n * kept local so feedback has no build-time dep on the i18n SDK. */\nexport interface I18nPluginContext {\n config: {\n apiBase?: string;\n projectUuid: string;\n defaultLocale: string;\n };\n}\nexport interface I18nPlugin {\n name: string;\n setup?: (ctx: I18nPluginContext) => void | (() => void);\n render?: () => ReactNode;\n}\n\nexport interface FeedbackController {\n open: () => void;\n close: () => void;\n /** The underlying client (advanced; usually unused). */\n client: FeedbackClient;\n}\n\nexport interface FeedbackPluginOptions {\n // NOTE: no `tosVersion` — it is an SDK build-time constant\n // (SDK_TOS_VERSION, task 616), sent automatically; never integrator-set.\n /** Override language; defaults to the i18n provider's defaultLocale. */\n language?: string;\n /** Override apiBase; defaults to the i18n provider's apiBase. */\n apiBase?: string;\n /** Override projectId; defaults to the i18n provider's projectUuid. */\n projectId?: string;\n /** Optional opaque end-user id (server generates one when absent). */\n endUserId?: string;\n /** Explicit on-screen keys (else the i18n key registry, if present). */\n keys?: DeclaredKey[];\n /** Receives the imperative { open, close } handle for the host CTA. */\n onReady?: (controller: FeedbackController) => void;\n /** Alt delivery: a ref object that receives the controller. */\n controllerRef?: { current: FeedbackController | null };\n /** Injected fetch (tests / RN polyfills). */\n fetchImpl?: typeof fetch;\n}\n\nfunction makeStore() {\n let open = false;\n const listeners = new Set<() => void>();\n return {\n isOpen: () => open,\n set(v: boolean) {\n if (open !== v) {\n open = v;\n listeners.forEach((l) => l());\n }\n },\n subscribe(l: () => void) {\n listeners.add(l);\n return () => listeners.delete(l);\n },\n };\n}\n\nexport function feedbackPlugin(options: FeedbackPluginOptions): I18nPlugin {\n const store = makeStore();\n let client: FeedbackClient | null = null;\n\n function Outlet() {\n const isOpen = useSyncExternalStore(\n store.subscribe,\n store.isOpen,\n store.isOpen,\n );\n if (!isOpen || !client || typeof document === \"undefined\") return null;\n const c = client;\n return createPortal(\n <FeedbackPanel\n client={c}\n keys={options.keys}\n onClose={() => {\n store.set(false);\n void c.flush();\n }}\n />,\n document.body,\n );\n }\n\n return {\n name: \"@verbumia/feedback\",\n setup(ctx) {\n client = new FeedbackClient({\n apiBase:\n options.apiBase ?? ctx.config.apiBase ?? \"https://api.verbumia.ca\",\n projectId: options.projectId ?? ctx.config.projectUuid,\n language: options.language ?? ctx.config.defaultLocale,\n endUserId: options.endUserId,\n fetchImpl: options.fetchImpl,\n });\n const controller: FeedbackController = {\n open: () => store.set(true),\n close: () => {\n store.set(false);\n void client?.flush();\n },\n client,\n };\n options.onReady?.(controller);\n if (options.controllerRef) options.controllerRef.current = controller;\n return () => {\n if (options.controllerRef) options.controllerRef.current = null;\n void client?.flush();\n };\n },\n render: () => <Outlet />,\n };\n}\n","import { useCallback, useEffect, useState } from \"react\";\n\nimport type { FeedbackClient } from \"../core/client\";\nimport { resolveKeys } from \"../core/keys\";\nimport type { DeclaredKey, FeedbackString } from \"../core/types\";\n\nconst C = {\n bg: \"#0b0f0e\",\n panel: \"#111714\",\n border: \"#1f2a25\",\n text: \"#e7f5ef\",\n dim: \"#8aa79b\",\n emerald: \"#10b981\",\n emeraldSoft: \"#34d399\",\n};\n\nexport function FeedbackPanel(props: {\n client: FeedbackClient;\n keys?: DeclaredKey[];\n onClose: () => void;\n}) {\n const { client, keys, onClose } = props;\n const [consented, setConsented] = useState(client.hasConsented);\n const [busy, setBusy] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [strings, setStrings] = useState<FeedbackString[]>([]);\n\n const loadStrings = useCallback(async () => {\n setBusy(true);\n setError(null);\n try {\n const resolved = resolveKeys(keys);\n const res = await client.getStrings({\n keys: resolved.length ? resolved : undefined,\n });\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]);\n\n useEffect(() => {\n if (consented) void loadStrings();\n }, [consented, loadStrings]);\n\n const accept = useCallback(async () => {\n setBusy(true);\n setError(null);\n try {\n await client.acceptTos();\n setConsented(true);\n } catch (e) {\n setError(e instanceof Error ? e.message : \"Could not accept the terms\");\n } finally {\n setBusy(false);\n }\n }, [client]);\n\n return (\n <div\n role=\"dialog\"\n aria-label=\"Translation feedback\"\n style={{\n position: \"fixed\",\n inset: 0,\n zIndex: 2147483600,\n background: \"rgba(0,0,0,.55)\",\n display: \"flex\",\n justifyContent: \"flex-end\",\n }}\n onClick={onClose}\n >\n <div\n onClick={(e) => e.stopPropagation()}\n style={{\n width: \"min(420px, 100%)\",\n height: \"100%\",\n background: C.bg,\n color: C.text,\n borderLeft: `1px solid ${C.border}`,\n display: \"flex\",\n flexDirection: \"column\",\n fontFamily: \"system-ui, sans-serif\",\n }}\n >\n <header\n style={{\n padding: \"16px 18px\",\n borderBottom: `1px solid ${C.border}`,\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n }}\n >\n <strong style={{ color: C.emeraldSoft }}>Translation feedback</strong>\n <button\n type=\"button\"\n onClick={onClose}\n aria-label=\"Close\"\n style={{\n background: \"transparent\",\n color: C.dim,\n border: \"none\",\n fontSize: 20,\n cursor: \"pointer\",\n }}\n >\n ×\n </button>\n </header>\n\n <div style={{ padding: 18, overflowY: \"auto\", flex: 1 }}>\n {error && (\n <p style={{ color: \"#f87171\", fontSize: 13 }}>{error}</p>\n )}\n\n {!consented ? (\n <ConsentStep\n version={client.tosVersion}\n busy={busy}\n onAccept={accept}\n />\n ) : busy && !strings.length ? (\n <p style={{ color: C.dim }}>Loading…</p>\n ) : !strings.length ? (\n <p style={{ color: C.dim }}>No strings to review on this view.</p>\n ) : (\n strings.map((s) => (\n <StringRow key={`${s.namespace}:${s.key}`} s={s} client={client} />\n ))\n )}\n </div>\n <footer\n style={{\n padding: \"10px 18px\",\n borderTop: `1px solid ${C.border}`,\n color: C.dim,\n fontSize: 11,\n }}\n >\n Powered by Verbumia\n </footer>\n </div>\n </div>\n );\n}\n\nfunction ConsentStep(props: {\n version?: string;\n busy: boolean;\n onAccept: () => void;\n}) {\n return (\n <div>\n <p style={{ lineHeight: 1.5, fontSize: 14 }}>\n Help improve the translations in this app. Your ratings and\n suggestions are sent to the app owner via Verbumia. Don’t submit\n personal or sensitive information.\n </p>\n <button\n type=\"button\"\n disabled={props.busy}\n onClick={props.onAccept}\n style={{\n marginTop: 12,\n width: \"100%\",\n background: C.emerald,\n color: \"#03110c\",\n border: \"none\",\n borderRadius: 8,\n padding: \"12px 14px\",\n fontWeight: 700,\n cursor: props.busy ? \"default\" : \"pointer\",\n opacity: props.busy ? 0.6 : 1,\n }}\n >\n {props.busy\n ? \"…\"\n : `I accept the terms${props.version ? ` (v${props.version})` : \"\"}`}\n </button>\n </div>\n );\n}\n\nfunction StringRow(props: { s: FeedbackString; client: FeedbackClient }) {\n const { s, client } = props;\n const [mine, setMine] = useState<number | null>(s.my_rating);\n const [showSuggest, setShowSuggest] = useState(false);\n const [text, setText] = useState(\"\");\n const [sent, setSent] = useState(false);\n\n const rate = (stars: number) => {\n setMine(stars);\n client.rate({\n namespace: s.namespace,\n key: s.key,\n language: client.language,\n translation_hash: s.translation_hash,\n stars,\n });\n };\n\n const submitSuggestion = () => {\n if (!text.trim()) return;\n client.suggest({\n namespace: s.namespace,\n key: s.key,\n language: client.language,\n translation_hash: s.translation_hash,\n suggested_text: text.trim(),\n });\n setSent(true);\n setShowSuggest(false);\n setText(\"\");\n };\n\n return (\n <div\n style={{\n background: C.panel,\n border: `1px solid ${C.border}`,\n borderRadius: 10,\n padding: 12,\n marginBottom: 10,\n }}\n >\n <div style={{ fontSize: 12, color: C.dim }}>\n {s.namespace} · {s.key}\n </div>\n <div style={{ margin: \"6px 0 10px\", fontSize: 15 }}>{s.value}</div>\n <div style={{ display: \"flex\", gap: 4 }}>\n {[1, 2, 3, 4, 5].map((n) => (\n <button\n key={n}\n type=\"button\"\n aria-label={`${n} star${n > 1 ? \"s\" : \"\"}`}\n onClick={() => rate(n)}\n style={{\n background: \"transparent\",\n border: \"none\",\n cursor: \"pointer\",\n fontSize: 20,\n color: mine && n <= mine ? C.emeraldSoft : C.border,\n }}\n >\n ★\n </button>\n ))}\n <button\n type=\"button\"\n onClick={() => setShowSuggest((v) => !v)}\n style={{\n marginLeft: \"auto\",\n background: \"transparent\",\n color: C.emeraldSoft,\n border: `1px solid ${C.border}`,\n borderRadius: 6,\n padding: \"4px 10px\",\n fontSize: 12,\n cursor: \"pointer\",\n }}\n >\n {sent ? \"Suggested ✓\" : \"Suggest\"}\n </button>\n </div>\n {showSuggest && (\n <div style={{ marginTop: 10 }}>\n <textarea\n value={text}\n onChange={(e) => setText(e.target.value)}\n rows={3}\n placeholder=\"Your suggested translation…\"\n style={{\n width: \"100%\",\n background: C.bg,\n color: C.text,\n border: `1px solid ${C.border}`,\n borderRadius: 6,\n padding: 8,\n fontSize: 14,\n resize: \"vertical\",\n }}\n />\n <button\n type=\"button\"\n onClick={submitSuggestion}\n style={{\n marginTop: 6,\n background: C.emerald,\n color: \"#03110c\",\n border: \"none\",\n borderRadius: 6,\n padding: \"8px 12px\",\n fontWeight: 700,\n cursor: \"pointer\",\n }}\n >\n Send suggestion\n </button>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["jsx"]}
|
|
1
|
+
{"version":3,"sources":["../../src/react/plugin.tsx","../../src/react/panel.tsx"],"sourcesContent":["/**\n * `@verbumia/feedback` as a PLUGIN of the `@verbumia/*-i18n` provider.\n *\n * Architecture (task 599): NO second React context. The customer adds\n * `feedbackPlugin(...)` to the i18n provider's `plugins` slot. The\n * provider calls `setup({ i18n, config })` once (we reuse its apiBase /\n * projectUuid / locale — no re-config) and renders our `render()` as an\n * ISOLATED sibling leaf. The panel's open/close lives in a tiny private\n * store the outlet subscribes to via useSyncExternalStore, so toggling\n * feedback NEVER re-renders the host app. The customer triggers it via\n * their own CTA through the imperative controller.\n */\nimport { createPortal } from \"react-dom\";\nimport { useSyncExternalStore, type ReactNode } from \"react\";\n\nimport { FeedbackClient } from \"../core/client\";\nimport type { DeclaredKey } from \"../core/types\";\nimport { FeedbackPanel } from \"./panel\";\n\n/** Structural mirror of `@verbumia/react-i18next`'s VerbumiaPlugin —\n * kept local so feedback has no build-time dep on the i18n SDK. */\nexport interface I18nPluginContext {\n config: {\n apiBase?: string;\n projectUuid: string;\n defaultLocale: string;\n };\n}\nexport interface I18nPlugin {\n name: string;\n setup?: (ctx: I18nPluginContext) => void | (() => void);\n render?: () => ReactNode;\n}\n\nexport interface FeedbackController {\n open: () => void;\n close: () => void;\n /** The underlying client (advanced; usually unused). */\n client: FeedbackClient;\n}\n\nexport interface FeedbackPluginOptions {\n // NOTE: no `tosVersion` — it is an SDK build-time constant\n // (SDK_TOS_VERSION, task 616), sent automatically; never integrator-set.\n /** Override language; defaults to the i18n provider's defaultLocale. */\n language?: string;\n /** Override apiBase; defaults to the i18n provider's apiBase. */\n apiBase?: string;\n /** Override projectId; defaults to the i18n provider's projectUuid. */\n projectId?: string;\n /** Optional opaque end-user id (server generates one when absent). */\n endUserId?: string;\n /** Explicit on-screen keys (else the i18n key registry, if present). */\n keys?: DeclaredKey[];\n /** Receives the imperative { open, close } handle for the host CTA. */\n onReady?: (controller: FeedbackController) => void;\n /** Alt delivery: a ref object that receives the controller. */\n controllerRef?: { current: FeedbackController | null };\n /** Injected fetch (tests / RN polyfills). */\n fetchImpl?: typeof fetch;\n}\n\nfunction makeStore() {\n let open = false;\n const listeners = new Set<() => void>();\n return {\n isOpen: () => open,\n set(v: boolean) {\n if (open !== v) {\n open = v;\n listeners.forEach((l) => l());\n }\n },\n subscribe(l: () => void) {\n listeners.add(l);\n return () => listeners.delete(l);\n },\n };\n}\n\nexport function feedbackPlugin(options: FeedbackPluginOptions): I18nPlugin {\n const store = makeStore();\n let client: FeedbackClient | null = null;\n\n function Outlet() {\n const isOpen = useSyncExternalStore(\n store.subscribe,\n store.isOpen,\n store.isOpen,\n );\n if (!isOpen || !client || typeof document === \"undefined\") return null;\n const c = client;\n return createPortal(\n <FeedbackPanel\n client={c}\n keys={options.keys}\n onClose={() => {\n store.set(false);\n void c.flush();\n }}\n />,\n document.body,\n );\n }\n\n return {\n name: \"@verbumia/feedback\",\n setup(ctx) {\n client = new FeedbackClient({\n apiBase:\n options.apiBase ?? ctx.config.apiBase ?? \"https://api.verbumia.ca\",\n projectId: options.projectId ?? ctx.config.projectUuid,\n language: options.language ?? ctx.config.defaultLocale,\n endUserId: options.endUserId,\n fetchImpl: options.fetchImpl,\n });\n const controller: FeedbackController = {\n open: () => store.set(true),\n close: () => {\n store.set(false);\n void client?.flush();\n },\n client,\n };\n options.onReady?.(controller);\n if (options.controllerRef) options.controllerRef.current = controller;\n return () => {\n if (options.controllerRef) options.controllerRef.current = null;\n void client?.flush();\n };\n },\n render: () => <Outlet />,\n };\n}\n","import { useCallback, useEffect, useState } from \"react\";\n\nimport type { FeedbackClient } from \"../core/client\";\nimport { resolveKeys } from \"../core/keys\";\nimport type { DeclaredKey, FeedbackString } from \"../core/types\";\n\nconst C = {\n bg: \"#0b0f0e\",\n panel: \"#111714\",\n border: \"#1f2a25\",\n text: \"#e7f5ef\",\n dim: \"#8aa79b\",\n emerald: \"#10b981\",\n emeraldSoft: \"#34d399\",\n};\n\nexport function FeedbackPanel(props: {\n client: FeedbackClient;\n keys?: DeclaredKey[];\n onClose: () => void;\n}) {\n const { client, keys, onClose } = props;\n const [consented, setConsented] = useState(client.hasConsented);\n const [busy, setBusy] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [strings, setStrings] = useState<FeedbackString[]>([]);\n\n const loadStrings = useCallback(async () => {\n setBusy(true);\n setError(null);\n try {\n // On-screen scoping is NORMATIVE (spec ltm 373, CONTRACT v5): show\n // ONLY rendered keys. Empty -> \"no strings on this view\"; never\n // fall back to fetching the whole project.\n const resolved = resolveKeys(keys);\n if (!resolved.length) {\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]);\n\n useEffect(() => {\n if (consented) void loadStrings();\n }, [consented, loadStrings]);\n\n const accept = useCallback(async () => {\n setBusy(true);\n setError(null);\n try {\n await client.acceptTos();\n setConsented(true);\n } catch (e) {\n setError(e instanceof Error ? e.message : \"Could not accept the terms\");\n } finally {\n setBusy(false);\n }\n }, [client]);\n\n return (\n <div\n role=\"dialog\"\n aria-label=\"Translation feedback\"\n style={{\n position: \"fixed\",\n inset: 0,\n zIndex: 2147483600,\n background: \"rgba(0,0,0,.55)\",\n display: \"flex\",\n justifyContent: \"flex-end\",\n }}\n onClick={onClose}\n >\n <div\n onClick={(e) => e.stopPropagation()}\n style={{\n width: \"min(420px, 100%)\",\n height: \"100%\",\n background: C.bg,\n color: C.text,\n borderLeft: `1px solid ${C.border}`,\n display: \"flex\",\n flexDirection: \"column\",\n fontFamily: \"system-ui, sans-serif\",\n }}\n >\n <header\n style={{\n padding: \"16px 18px\",\n borderBottom: `1px solid ${C.border}`,\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n }}\n >\n <strong style={{ color: C.emeraldSoft }}>Translation feedback</strong>\n <button\n type=\"button\"\n onClick={onClose}\n aria-label=\"Close\"\n style={{\n background: \"transparent\",\n color: C.dim,\n border: \"none\",\n fontSize: 20,\n cursor: \"pointer\",\n }}\n >\n ×\n </button>\n </header>\n\n <div style={{ padding: 18, overflowY: \"auto\", flex: 1 }}>\n {error && (\n <p style={{ color: \"#f87171\", fontSize: 13 }}>{error}</p>\n )}\n\n {!consented ? (\n <ConsentStep\n version={client.tosVersion}\n busy={busy}\n onAccept={accept}\n />\n ) : busy && !strings.length ? (\n <p style={{ color: C.dim }}>Loading…</p>\n ) : !strings.length ? (\n <p style={{ color: C.dim }}>No strings to review on this view.</p>\n ) : (\n strings.map((s) => (\n <StringRow key={`${s.namespace}:${s.key}`} s={s} client={client} />\n ))\n )}\n </div>\n <footer\n style={{\n padding: \"10px 18px\",\n borderTop: `1px solid ${C.border}`,\n color: C.dim,\n fontSize: 11,\n }}\n >\n Powered by Verbumia\n </footer>\n </div>\n </div>\n );\n}\n\nfunction ConsentStep(props: {\n version?: string;\n busy: boolean;\n onAccept: () => void;\n}) {\n return (\n <div>\n <p style={{ lineHeight: 1.5, fontSize: 14 }}>\n Help improve the translations in this app. Your ratings and\n suggestions are sent to the app owner via Verbumia. Don’t submit\n personal or sensitive information.\n </p>\n <button\n type=\"button\"\n disabled={props.busy}\n onClick={props.onAccept}\n style={{\n marginTop: 12,\n width: \"100%\",\n background: C.emerald,\n color: \"#03110c\",\n border: \"none\",\n borderRadius: 8,\n padding: \"12px 14px\",\n fontWeight: 700,\n cursor: props.busy ? \"default\" : \"pointer\",\n opacity: props.busy ? 0.6 : 1,\n }}\n >\n {props.busy\n ? \"…\"\n : `I accept the terms${props.version ? ` (v${props.version})` : \"\"}`}\n </button>\n </div>\n );\n}\n\nfunction StringRow(props: { s: FeedbackString; client: FeedbackClient }) {\n const { s, client } = props;\n const [mine, setMine] = useState<number | null>(s.my_rating);\n const [showSuggest, setShowSuggest] = useState(false);\n const [text, setText] = useState(\"\");\n const [sent, setSent] = useState(false);\n\n const rate = (stars: number) => {\n setMine(stars);\n client.rate({\n namespace: s.namespace,\n key: s.key,\n language: client.language,\n translation_hash: s.translation_hash,\n stars,\n });\n };\n\n const submitSuggestion = () => {\n if (!text.trim()) return;\n client.suggest({\n namespace: s.namespace,\n key: s.key,\n language: client.language,\n translation_hash: s.translation_hash,\n suggested_text: text.trim(),\n });\n setSent(true);\n setShowSuggest(false);\n setText(\"\");\n };\n\n return (\n <div\n style={{\n background: C.panel,\n border: `1px solid ${C.border}`,\n borderRadius: 10,\n padding: 12,\n marginBottom: 10,\n }}\n >\n <div style={{ fontSize: 12, color: C.dim }}>\n {s.namespace} · {s.key}\n </div>\n <div style={{ margin: \"6px 0 10px\", fontSize: 15 }}>{s.value}</div>\n <div style={{ display: \"flex\", gap: 4 }}>\n {[1, 2, 3, 4, 5].map((n) => (\n <button\n key={n}\n type=\"button\"\n aria-label={`${n} star${n > 1 ? \"s\" : \"\"}`}\n onClick={() => rate(n)}\n style={{\n background: \"transparent\",\n border: \"none\",\n cursor: \"pointer\",\n fontSize: 20,\n color: mine && n <= mine ? C.emeraldSoft : C.border,\n }}\n >\n ★\n </button>\n ))}\n <button\n type=\"button\"\n onClick={() => setShowSuggest((v) => !v)}\n style={{\n marginLeft: \"auto\",\n background: \"transparent\",\n color: C.emeraldSoft,\n border: `1px solid ${C.border}`,\n borderRadius: 6,\n padding: \"4px 10px\",\n fontSize: 12,\n cursor: \"pointer\",\n }}\n >\n {sent ? \"Suggested ✓\" : \"Suggest\"}\n </button>\n </div>\n {showSuggest && (\n <div style={{ marginTop: 10 }}>\n <textarea\n value={text}\n onChange={(e) => setText(e.target.value)}\n rows={3}\n placeholder=\"Your suggested translation…\"\n style={{\n width: \"100%\",\n background: C.bg,\n color: C.text,\n border: `1px solid ${C.border}`,\n borderRadius: 6,\n padding: 8,\n fontSize: 14,\n resize: \"vertical\",\n }}\n />\n <button\n type=\"button\"\n onClick={submitSuggestion}\n style={{\n marginTop: 6,\n background: C.emerald,\n color: \"#03110c\",\n border: \"none\",\n borderRadius: 6,\n padding: \"8px 12px\",\n fontWeight: 700,\n cursor: \"pointer\",\n }}\n >\n Send suggestion\n </button>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["jsx"]}
|
package/dist/svelte/index.cjs
CHANGED
|
@@ -44,7 +44,7 @@ var SDK_TOS_VERSION = "2026-05-18";
|
|
|
44
44
|
|
|
45
45
|
// src/core/client.ts
|
|
46
46
|
var SDK_LIB = "@verbumia/feedback";
|
|
47
|
-
var SDK_VER = "0.1.
|
|
47
|
+
var SDK_VER = "0.1.1";
|
|
48
48
|
var FeedbackClient = class {
|
|
49
49
|
cfg;
|
|
50
50
|
fetchImpl;
|
|
@@ -272,9 +272,11 @@ function createFeedback(config) {
|
|
|
272
272
|
async function loadStrings() {
|
|
273
273
|
if (!client.hasConsented) await client.acceptTos();
|
|
274
274
|
const resolved = resolveKeys(config.keys);
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
275
|
+
if (!resolved.length) {
|
|
276
|
+
strings.set([]);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const r = await client.getStrings({ keys: resolved });
|
|
278
280
|
strings.set(r.strings);
|
|
279
281
|
}
|
|
280
282
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/svelte/index.ts","../../src/core/types.ts","../../src/core/tos.ts","../../src/core/client.ts","../../src/core/keys.ts"],"sourcesContent":["/**\n * `@verbumia/feedback/svelte` — idiomatic Svelte adapter over the FROZEN\n * /core FeedbackClient (task 611, master-approved). Host-i18n\n * plugin-slot half is descoped (no @verbumia/svelte-i18n source), so\n * this is a standalone HEADLESS store adapter (idiomatic Svelte = stores;\n * the consumer renders its own panel — keeps the published build pure-TS,\n * no Svelte compiler needed). ISOLATED open-state (a per-instance\n * writable — toggling it never re-renders the host app). sessionId is\n * server-minted (no client groupingKey). Mirrors React-plugin principles.\n */\nimport { writable, type Writable } from \"svelte/store\";\n\nimport { FeedbackClient } from \"../core/client\";\nimport { resolveKeys } from \"../core/keys\";\nimport type {\n DeclaredKey,\n FeedbackString,\n RatingInput,\n SuggestionInput,\n} from \"../core/types\";\n\nexport interface SvelteFeedbackConfig {\n apiBase: string;\n projectId: string;\n language: string;\n // No tosVersion — SDK build-time constant (SDK_TOS_VERSION, task 616).\n endUserId?: string;\n keys?: DeclaredKey[];\n fetchImpl?: typeof fetch;\n}\n\nexport interface SvelteFeedback {\n client: FeedbackClient;\n /** Isolated open-state store. Bind your own panel to it. */\n isOpen: Writable<boolean>;\n /** Strings store — populated by loadStrings(). */\n strings: Writable<FeedbackString[]>;\n open: () => Promise<void>;\n close: () => void;\n loadStrings: () => Promise<void>;\n rate: (r: RatingInput) => void;\n suggest: (s: SuggestionInput) => void;\n}\n\nexport function createFeedback(config: SvelteFeedbackConfig): SvelteFeedback {\n const client = new FeedbackClient({\n apiBase: config.apiBase,\n projectId: config.projectId,\n language: config.language,\n endUserId: config.endUserId,\n fetchImpl: config.fetchImpl,\n });\n const isOpen = writable(false);\n const strings = writable<FeedbackString[]>([]);\n\n async function loadStrings(): Promise<void> {\n if (!client.hasConsented) await client.acceptTos();\n const resolved = resolveKeys(config.keys);\n const r = await client.getStrings({\n keys: resolved.length ? resolved : undefined,\n });\n strings.set(r.strings);\n }\n\n return {\n client,\n isOpen,\n strings,\n async open() {\n isOpen.set(true);\n try {\n await loadStrings();\n } catch {\n /* best-effort — never break the host app */\n }\n },\n close() {\n isOpen.set(false);\n void client.flush();\n },\n loadStrings,\n rate: (r) => client.rate(r),\n suggest: (s) => client.suggest(s),\n };\n}\n\nexport { FeedbackClient } from \"../core/client\";\nexport type {\n DeclaredKey, FeedbackString, RatingInput, SuggestionInput,\n TokenBundle, StringsResponse, BatchResponse,\n} from \"../core/types\";\nexport { FeedbackError } from \"../core/types\";\n","/**\n * Wire types for the Verbumia End-User Translation Evaluation API.\n * Canonical reference: ./CONTRACT.md (frozen). Backend task 591.\n */\n\nexport interface FeedbackConfig {\n /** API base, no trailing slash needed. e.g. https://api.verbumia.ca */\n apiBase: string;\n /** Public project UUID the widget targets. */\n projectId: string;\n /**\n * sessionId / grouping_key is MINTED SERVER-SIDE by the Verbumia\n * backend at POST /v1/feedback/tos and returned in the token bundle\n * (`TokenBundle.grouping_key`). The widget receives + sends it; it\n * MUST NOT self-generate it. There is intentionally no client config\n * field for it.\n */\n /**\n * NOTE: there is intentionally NO `tosVersion` field. The ToS version\n * is a BUILD-TIME constant baked into @verbumia/feedback at release\n * (`SDK_TOS_VERSION`, task 616) — integrators do not set it; the SDK\n * sends it automatically.\n */\n /** Optional opaque end-user id; server generates one when absent. */\n endUserId?: string;\n /** BCP-47 language the widget rates strings in (e.g. \"fr\"). */\n language: string;\n /** Optional locale tag stored for analytics. */\n locale?: string;\n /** Debounce window (ms) before a queued batch is flushed. Default 1500. */\n flushDebounceMs?: number;\n /** Max queued items before an immediate flush. Default 20. */\n maxBatch?: number;\n /** Injected fetch (tests / RN polyfills). Defaults to global fetch. */\n fetchImpl?: typeof fetch;\n}\n\nexport interface TokenBundle {\n access_token: string;\n token_type: \"Bearer\";\n expires_in: number;\n refresh_token: string;\n refresh_expires_in: number;\n end_user_id: string;\n tos_version: string;\n grouping_key: string;\n}\n\nexport interface FeedbackString {\n namespace: string;\n key: string;\n key_uuid: string;\n language_uuid: string;\n value: string;\n translation_hash: string;\n avg_stars: number | null;\n ratings_count: number;\n my_rating: number | null;\n}\n\nexport interface StringsResponse {\n project_id: string;\n language: string;\n strings: FeedbackString[];\n}\n\n/** A 5-star rating queued for a rendered string variant. */\nexport interface RatingInput {\n namespace: string;\n key: string;\n language: string;\n translation_hash: string;\n stars: number; // 1..5\n}\n\n/** A suggested alternative translation queued for moderation. */\nexport interface SuggestionInput {\n namespace: string;\n key: string;\n language: string;\n translation_hash: string;\n suggested_text: string;\n comment?: string;\n}\n\nexport interface BatchResponse {\n accepted: number;\n rejected: number;\n items: Array<Record<string, unknown>>;\n}\n\n/** A key the host app declares as on-screen. */\nexport interface DeclaredKey {\n namespace: string;\n key: string;\n}\n\nexport class FeedbackError extends Error {\n constructor(\n message: string,\n readonly status?: number,\n readonly code?: string,\n ) {\n super(message);\n this.name = \"FeedbackError\";\n }\n}\n","/**\n * BUILD-TIME ToS version constant (task 616, human decision option B).\n *\n * This is baked into the @verbumia/feedback PACKAGE at release. It is\n * NOT an integrator-set config field and NOT server-driven: a stale\n * (e.g. native) app must record consent to the ToS version IT shipped\n * and displayed, for legal correctness — never backend-latest.\n *\n * Releasing a ToS-TEXT change ⇒ bump this constant + cut a new\n * @verbumia/feedback release (per the human/real-CI publish handoff).\n * The backend accepts any version in its acceptable set and records it.\n */\nexport const SDK_TOS_VERSION = \"2026-05-18\";\n","/**\n * Framework-agnostic Verbumia feedback client.\n *\n * Responsibilities:\n * - versioned-ToS acceptance -> scoped end-user JWT bootstrap\n * - transparent access-token refresh (rotating refresh token)\n * - fetch on-screen strings for the widget\n * - debounced + size-capped batched POST of ratings / suggestions,\n * mirroring the missing-handler transport contract (ltm 280):\n * best-effort, never throws into the host app render path.\n *\n * The React and React Native entry points are thin UI shells over this.\n */\n\nimport {\n type BatchResponse,\n type FeedbackConfig,\n FeedbackError,\n type RatingInput,\n type StringsResponse,\n type SuggestionInput,\n type TokenBundle,\n} from \"./types\";\nimport { SDK_TOS_VERSION } from \"./tos\";\n\nconst SDK_LIB = \"@verbumia/feedback\";\nconst SDK_VER = \"0.1.0\";\n\ntype QueueItem =\n | { kind: \"rating\"; payload: RatingInput }\n | { kind: \"suggestion\"; payload: SuggestionInput };\n\nexport class FeedbackClient {\n private cfg: Required<\n Pick<FeedbackConfig, \"flushDebounceMs\" | \"maxBatch\">\n > &\n FeedbackConfig;\n private fetchImpl: typeof fetch;\n private tokens: TokenBundle | null = null;\n private queue: QueueItem[] = [];\n private timer: ReturnType<typeof setTimeout> | null = null;\n private bootstrapping: Promise<TokenBundle> | null = null;\n\n constructor(config: FeedbackConfig) {\n this.cfg = {\n flushDebounceMs: 1500,\n maxBatch: 20,\n ...config,\n };\n const f = config.fetchImpl ?? globalThis.fetch;\n if (!f) {\n throw new FeedbackError(\n \"no fetch implementation available; pass config.fetchImpl\",\n );\n }\n this.fetchImpl = f.bind(globalThis);\n }\n\n private base(): string {\n return this.cfg.apiBase.replace(/\\/+$/, \"\");\n }\n\n get endUserId(): string | undefined {\n return this.tokens?.end_user_id ?? this.cfg.endUserId;\n }\n\n get hasConsented(): boolean {\n return this.tokens !== null;\n }\n\n /** Server-minted sessionId / grouping_key (from the token bundle).\n * Available only after `acceptTos()`; never client-generated. */\n get sessionId(): string | undefined {\n return this.tokens?.grouping_key;\n }\n\n /** BCP-47 language the widget is rating strings in. */\n get language(): string {\n return this.cfg.language;\n }\n\n /** ToS version the end user is asked to accept — the SDK's\n * build-time constant (task 616). NOT integrator/server set. */\n get tosVersion(): string {\n return SDK_TOS_VERSION;\n }\n\n /**\n * Accept the ToS and bootstrap a scoped token. Idempotent: a second call\n * returns the in-flight / existing bundle rather than re-accepting.\n */\n async acceptTos(): Promise<TokenBundle> {\n if (this.tokens) return this.tokens;\n if (this.bootstrapping) return this.bootstrapping;\n this.bootstrapping = (async () => {\n const res = await this.fetchImpl(`${this.base()}/v1/feedback/tos`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n // NO grouping_key: server-minted (task 599). It comes back in\n // the token bundle and is bound into the JWT server-side.\n project_id: this.cfg.projectId,\n end_user_id: this.cfg.endUserId,\n tos_version: SDK_TOS_VERSION,\n locale: this.cfg.locale,\n }),\n });\n if (!res.ok) throw await this.problem(res, \"tos acceptance failed\");\n this.tokens = (await res.json()) as TokenBundle;\n return this.tokens;\n })();\n try {\n return await this.bootstrapping;\n } finally {\n this.bootstrapping = null;\n }\n }\n\n private async refresh(): Promise<void> {\n if (!this.tokens) throw new FeedbackError(\"not consented\");\n const res = await this.fetchImpl(\n `${this.base()}/v1/feedback/token/refresh`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ refresh_token: this.tokens.refresh_token }),\n },\n );\n if (!res.ok) {\n this.tokens = null; // force a fresh ToS step\n throw await this.problem(res, \"token refresh failed\");\n }\n this.tokens = (await res.json()) as TokenBundle;\n }\n\n /** Authenticated fetch with a single transparent refresh-on-401 retry. */\n private async authed(\n path: string,\n init: RequestInit,\n retry = true,\n ): Promise<Response> {\n if (!this.tokens) await this.acceptTos();\n const res = await this.fetchImpl(`${this.base()}${path}`, {\n ...init,\n headers: {\n ...(init.headers ?? {}),\n Authorization: `Bearer ${this.tokens!.access_token}`,\n },\n });\n if (res.status === 401 && retry) {\n await this.refresh();\n return this.authed(path, init, false);\n }\n return res;\n }\n\n /** Strings rendered on the current view, with this end user's prior rating. */\n async getStrings(opts?: {\n keys?: Array<{ namespace: string; key: string }>;\n namespace?: string;\n limit?: number;\n }): Promise<StringsResponse> {\n const qs = new URLSearchParams({ language: this.cfg.language });\n if (opts?.keys?.length) {\n qs.set(\"keys\", opts.keys.map((k) => `${k.namespace}:${k.key}`).join(\",\"));\n }\n if (opts?.namespace) qs.set(\"namespace\", opts.namespace);\n if (opts?.limit) qs.set(\"limit\", String(opts.limit));\n const res = await this.authed(`/v1/feedback/strings?${qs}`, {\n method: \"GET\",\n });\n if (!res.ok) throw await this.problem(res, \"failed to load strings\");\n return (await res.json()) as StringsResponse;\n }\n\n /** Queue a rating; flushed on debounce or when the batch fills. */\n rate(payload: RatingInput): void {\n this.enqueue({ kind: \"rating\", payload });\n }\n\n /** Queue a suggestion; flushed on debounce or when the batch fills. */\n suggest(payload: SuggestionInput): void {\n this.enqueue({ kind: \"suggestion\", payload });\n }\n\n private enqueue(item: QueueItem): void {\n this.queue.push(item);\n if (this.queue.length >= this.cfg.maxBatch) {\n void this.flush();\n return;\n }\n if (this.timer) clearTimeout(this.timer);\n this.timer = setTimeout(() => void this.flush(), this.cfg.flushDebounceMs);\n }\n\n /**\n * Flush queued items. Best-effort: a transport/auth failure re-queues the\n * batch once and swallows the error so the host app never sees a throw.\n */\n async flush(): Promise<void> {\n if (this.timer) {\n clearTimeout(this.timer);\n this.timer = null;\n }\n if (!this.queue.length) return;\n const batch = this.queue;\n this.queue = [];\n const ratings = batch\n .filter((b) => b.kind === \"rating\")\n .map((b) => b.payload as RatingInput);\n const suggestions = batch\n .filter((b) => b.kind === \"suggestion\")\n .map((b) => b.payload as SuggestionInput);\n try {\n if (ratings.length) {\n await this.postBatch(\"/v1/feedback/ratings\", { ratings });\n }\n if (suggestions.length) {\n await this.postBatch(\"/v1/feedback/suggestions\", { suggestions });\n }\n } catch {\n // Re-queue once; feedback must never break the host app.\n this.queue.unshift(...batch);\n }\n }\n\n private async postBatch(\n path: string,\n body: unknown,\n ): Promise<BatchResponse> {\n const res = await this.authed(path, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", \"X-SDK\": `${SDK_LIB}@${SDK_VER}` },\n body: JSON.stringify(body),\n });\n if (!res.ok) throw await this.problem(res, \"batch post failed\");\n return (await res.json()) as BatchResponse;\n }\n\n private async problem(res: Response, fallback: string): Promise<FeedbackError> {\n let code: string | undefined;\n let detail = fallback;\n try {\n const body = (await res.json()) as { code?: string; detail?: string };\n code = body.code;\n if (body.detail) detail = body.detail;\n } catch {\n /* non-JSON body */\n }\n return new FeedbackError(detail, res.status, code);\n }\n}\n","/**\n * On-screen key discovery.\n *\n * Preferred source: the `@verbumia/*-i18n` SDK exposes a lightweight key\n * registry of keys it has rendered. When that registry is present on the\n * global (the i18n SDK publishes `globalThis.__verbumia_key_registry__`),\n * we read the keys touched on the current view. Otherwise the host app\n * passes an explicit `keys` list — the always-available fallback.\n *\n * The registry contract is intentionally tiny so any framework port of the\n * i18n SDK can implement it without depending on this package.\n */\n\nimport type { DeclaredKey } from \"./types\";\n\nconst REGISTRY_GLOBAL = \"__verbumia_key_registry__\";\n\ninterface KeyRegistry {\n /** Returns the keys rendered since the last `reset()` (or ever). */\n snapshot(): DeclaredKey[];\n reset?(): void;\n}\n\nfunction getRegistry(): KeyRegistry | null {\n const g = globalThis as Record<string, unknown>;\n const reg = g[REGISTRY_GLOBAL];\n if (\n reg &&\n typeof (reg as KeyRegistry).snapshot === \"function\"\n ) {\n return reg as KeyRegistry;\n }\n return null;\n}\n\n/** True when a compatible `@verbumia/*-i18n` registry is detectable. */\nexport function hasKeyRegistry(): boolean {\n return getRegistry() !== null;\n}\n\n/**\n * Resolve the on-screen keys: explicit `keys` prop always wins (it is the\n * customer's authoritative declaration); otherwise fall back to the i18n\n * registry snapshot; otherwise an empty list (widget shows \"no strings\").\n */\nexport function resolveKeys(explicit?: DeclaredKey[]): DeclaredKey[] {\n if (explicit && explicit.length) return dedupe(explicit);\n const reg = getRegistry();\n if (reg) return dedupe(reg.snapshot());\n return [];\n}\n\nfunction dedupe(keys: DeclaredKey[]): DeclaredKey[] {\n const seen = new Set<string>();\n const out: DeclaredKey[] = [];\n for (const k of keys) {\n const id = `${k.namespace}:${k.key}`;\n if (!seen.has(id)) {\n seen.add(id);\n out.push(k);\n }\n }\n return out;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/svelte/index.ts","../../src/core/types.ts","../../src/core/tos.ts","../../src/core/client.ts","../../src/core/keys.ts"],"sourcesContent":["/**\n * `@verbumia/feedback/svelte` — idiomatic Svelte adapter over the FROZEN\n * /core FeedbackClient (task 611, master-approved). Host-i18n\n * plugin-slot half is descoped (no @verbumia/svelte-i18n source), so\n * this is a standalone HEADLESS store adapter (idiomatic Svelte = stores;\n * the consumer renders its own panel — keeps the published build pure-TS,\n * no Svelte compiler needed). ISOLATED open-state (a per-instance\n * writable — toggling it never re-renders the host app). sessionId is\n * server-minted (no client groupingKey). Mirrors React-plugin principles.\n */\nimport { writable, type Writable } from \"svelte/store\";\n\nimport { FeedbackClient } from \"../core/client\";\nimport { resolveKeys } from \"../core/keys\";\nimport type {\n DeclaredKey,\n FeedbackString,\n RatingInput,\n SuggestionInput,\n} from \"../core/types\";\n\nexport interface SvelteFeedbackConfig {\n apiBase: string;\n projectId: string;\n language: string;\n // No tosVersion — SDK build-time constant (SDK_TOS_VERSION, task 616).\n endUserId?: string;\n keys?: DeclaredKey[];\n fetchImpl?: typeof fetch;\n}\n\nexport interface SvelteFeedback {\n client: FeedbackClient;\n /** Isolated open-state store. Bind your own panel to it. */\n isOpen: Writable<boolean>;\n /** Strings store — populated by loadStrings(). */\n strings: Writable<FeedbackString[]>;\n open: () => Promise<void>;\n close: () => void;\n loadStrings: () => Promise<void>;\n rate: (r: RatingInput) => void;\n suggest: (s: SuggestionInput) => void;\n}\n\nexport function createFeedback(config: SvelteFeedbackConfig): SvelteFeedback {\n const client = new FeedbackClient({\n apiBase: config.apiBase,\n projectId: config.projectId,\n language: config.language,\n endUserId: config.endUserId,\n fetchImpl: config.fetchImpl,\n });\n const isOpen = writable(false);\n const strings = writable<FeedbackString[]>([]);\n\n async function loadStrings(): Promise<void> {\n if (!client.hasConsented) await client.acceptTos();\n // On-screen scoping is NORMATIVE (spec ltm 373, CONTRACT v5): ONLY\n // rendered keys; empty -> show none, never fetch the whole project.\n const resolved = resolveKeys(config.keys);\n if (!resolved.length) {\n strings.set([]);\n return;\n }\n const r = await client.getStrings({ keys: resolved });\n strings.set(r.strings);\n }\n\n return {\n client,\n isOpen,\n strings,\n async open() {\n isOpen.set(true);\n try {\n await loadStrings();\n } catch {\n /* best-effort — never break the host app */\n }\n },\n close() {\n isOpen.set(false);\n void client.flush();\n },\n loadStrings,\n rate: (r) => client.rate(r),\n suggest: (s) => client.suggest(s),\n };\n}\n\nexport { FeedbackClient } from \"../core/client\";\nexport type {\n DeclaredKey, FeedbackString, RatingInput, SuggestionInput,\n TokenBundle, StringsResponse, BatchResponse,\n} from \"../core/types\";\nexport { FeedbackError } from \"../core/types\";\n","/**\n * Wire types for the Verbumia End-User Translation Evaluation API.\n * Canonical reference: ./CONTRACT.md (frozen). Backend task 591.\n */\n\nexport interface FeedbackConfig {\n /** API base, no trailing slash needed. e.g. https://api.verbumia.ca */\n apiBase: string;\n /** Public project UUID the widget targets. */\n projectId: string;\n /**\n * sessionId / grouping_key is MINTED SERVER-SIDE by the Verbumia\n * backend at POST /v1/feedback/tos and returned in the token bundle\n * (`TokenBundle.grouping_key`). The widget receives + sends it; it\n * MUST NOT self-generate it. There is intentionally no client config\n * field for it.\n */\n /**\n * NOTE: there is intentionally NO `tosVersion` field. The ToS version\n * is a BUILD-TIME constant baked into @verbumia/feedback at release\n * (`SDK_TOS_VERSION`, task 616) — integrators do not set it; the SDK\n * sends it automatically.\n */\n /** Optional opaque end-user id; server generates one when absent. */\n endUserId?: string;\n /** BCP-47 language the widget rates strings in (e.g. \"fr\"). */\n language: string;\n /** Optional locale tag stored for analytics. */\n locale?: string;\n /** Debounce window (ms) before a queued batch is flushed. Default 1500. */\n flushDebounceMs?: number;\n /** Max queued items before an immediate flush. Default 20. */\n maxBatch?: number;\n /** Injected fetch (tests / RN polyfills). Defaults to global fetch. */\n fetchImpl?: typeof fetch;\n}\n\nexport interface TokenBundle {\n access_token: string;\n token_type: \"Bearer\";\n expires_in: number;\n refresh_token: string;\n refresh_expires_in: number;\n end_user_id: string;\n tos_version: string;\n grouping_key: string;\n}\n\nexport interface FeedbackString {\n namespace: string;\n key: string;\n key_uuid: string;\n language_uuid: string;\n value: string;\n translation_hash: string;\n avg_stars: number | null;\n ratings_count: number;\n my_rating: number | null;\n}\n\nexport interface StringsResponse {\n project_id: string;\n language: string;\n strings: FeedbackString[];\n}\n\n/** A 5-star rating queued for a rendered string variant. */\nexport interface RatingInput {\n namespace: string;\n key: string;\n language: string;\n translation_hash: string;\n stars: number; // 1..5\n}\n\n/** A suggested alternative translation queued for moderation. */\nexport interface SuggestionInput {\n namespace: string;\n key: string;\n language: string;\n translation_hash: string;\n suggested_text: string;\n comment?: string;\n}\n\nexport interface BatchResponse {\n accepted: number;\n rejected: number;\n items: Array<Record<string, unknown>>;\n}\n\n/** A key the host app declares as on-screen. */\nexport interface DeclaredKey {\n namespace: string;\n key: string;\n}\n\nexport class FeedbackError extends Error {\n constructor(\n message: string,\n readonly status?: number,\n readonly code?: string,\n ) {\n super(message);\n this.name = \"FeedbackError\";\n }\n}\n","/**\n * BUILD-TIME ToS version constant (task 616, human decision option B).\n *\n * This is baked into the @verbumia/feedback PACKAGE at release. It is\n * NOT an integrator-set config field and NOT server-driven: a stale\n * (e.g. native) app must record consent to the ToS version IT shipped\n * and displayed, for legal correctness — never backend-latest.\n *\n * Releasing a ToS-TEXT change ⇒ bump this constant + cut a new\n * @verbumia/feedback release (per the human/real-CI publish handoff).\n * The backend accepts any version in its acceptable set and records it.\n */\nexport const SDK_TOS_VERSION = \"2026-05-18\";\n","/**\n * Framework-agnostic Verbumia feedback client.\n *\n * Responsibilities:\n * - versioned-ToS acceptance -> scoped end-user JWT bootstrap\n * - transparent access-token refresh (rotating refresh token)\n * - fetch on-screen strings for the widget\n * - debounced + size-capped batched POST of ratings / suggestions,\n * mirroring the missing-handler transport contract (ltm 280):\n * best-effort, never throws into the host app render path.\n *\n * The React and React Native entry points are thin UI shells over this.\n */\n\nimport {\n type BatchResponse,\n type FeedbackConfig,\n FeedbackError,\n type RatingInput,\n type StringsResponse,\n type SuggestionInput,\n type TokenBundle,\n} from \"./types\";\nimport { SDK_TOS_VERSION } from \"./tos\";\n\nconst SDK_LIB = \"@verbumia/feedback\";\nconst SDK_VER = \"0.1.1\";\n\ntype QueueItem =\n | { kind: \"rating\"; payload: RatingInput }\n | { kind: \"suggestion\"; payload: SuggestionInput };\n\nexport class FeedbackClient {\n private cfg: Required<\n Pick<FeedbackConfig, \"flushDebounceMs\" | \"maxBatch\">\n > &\n FeedbackConfig;\n private fetchImpl: typeof fetch;\n private tokens: TokenBundle | null = null;\n private queue: QueueItem[] = [];\n private timer: ReturnType<typeof setTimeout> | null = null;\n private bootstrapping: Promise<TokenBundle> | null = null;\n\n constructor(config: FeedbackConfig) {\n this.cfg = {\n flushDebounceMs: 1500,\n maxBatch: 20,\n ...config,\n };\n const f = config.fetchImpl ?? globalThis.fetch;\n if (!f) {\n throw new FeedbackError(\n \"no fetch implementation available; pass config.fetchImpl\",\n );\n }\n this.fetchImpl = f.bind(globalThis);\n }\n\n private base(): string {\n return this.cfg.apiBase.replace(/\\/+$/, \"\");\n }\n\n get endUserId(): string | undefined {\n return this.tokens?.end_user_id ?? this.cfg.endUserId;\n }\n\n get hasConsented(): boolean {\n return this.tokens !== null;\n }\n\n /** Server-minted sessionId / grouping_key (from the token bundle).\n * Available only after `acceptTos()`; never client-generated. */\n get sessionId(): string | undefined {\n return this.tokens?.grouping_key;\n }\n\n /** BCP-47 language the widget is rating strings in. */\n get language(): string {\n return this.cfg.language;\n }\n\n /** ToS version the end user is asked to accept — the SDK's\n * build-time constant (task 616). NOT integrator/server set. */\n get tosVersion(): string {\n return SDK_TOS_VERSION;\n }\n\n /**\n * Accept the ToS and bootstrap a scoped token. Idempotent: a second call\n * returns the in-flight / existing bundle rather than re-accepting.\n */\n async acceptTos(): Promise<TokenBundle> {\n if (this.tokens) return this.tokens;\n if (this.bootstrapping) return this.bootstrapping;\n this.bootstrapping = (async () => {\n const res = await this.fetchImpl(`${this.base()}/v1/feedback/tos`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n // NO grouping_key: server-minted (task 599). It comes back in\n // the token bundle and is bound into the JWT server-side.\n project_id: this.cfg.projectId,\n end_user_id: this.cfg.endUserId,\n tos_version: SDK_TOS_VERSION,\n locale: this.cfg.locale,\n }),\n });\n if (!res.ok) throw await this.problem(res, \"tos acceptance failed\");\n this.tokens = (await res.json()) as TokenBundle;\n return this.tokens;\n })();\n try {\n return await this.bootstrapping;\n } finally {\n this.bootstrapping = null;\n }\n }\n\n private async refresh(): Promise<void> {\n if (!this.tokens) throw new FeedbackError(\"not consented\");\n const res = await this.fetchImpl(\n `${this.base()}/v1/feedback/token/refresh`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ refresh_token: this.tokens.refresh_token }),\n },\n );\n if (!res.ok) {\n this.tokens = null; // force a fresh ToS step\n throw await this.problem(res, \"token refresh failed\");\n }\n this.tokens = (await res.json()) as TokenBundle;\n }\n\n /** Authenticated fetch with a single transparent refresh-on-401 retry. */\n private async authed(\n path: string,\n init: RequestInit,\n retry = true,\n ): Promise<Response> {\n if (!this.tokens) await this.acceptTos();\n const res = await this.fetchImpl(`${this.base()}${path}`, {\n ...init,\n headers: {\n ...(init.headers ?? {}),\n Authorization: `Bearer ${this.tokens!.access_token}`,\n },\n });\n if (res.status === 401 && retry) {\n await this.refresh();\n return this.authed(path, init, false);\n }\n return res;\n }\n\n /** Strings rendered on the current view, with this end user's prior rating. */\n async getStrings(opts?: {\n keys?: Array<{ namespace: string; key: string }>;\n namespace?: string;\n limit?: number;\n }): Promise<StringsResponse> {\n const qs = new URLSearchParams({ language: this.cfg.language });\n if (opts?.keys?.length) {\n qs.set(\"keys\", opts.keys.map((k) => `${k.namespace}:${k.key}`).join(\",\"));\n }\n if (opts?.namespace) qs.set(\"namespace\", opts.namespace);\n if (opts?.limit) qs.set(\"limit\", String(opts.limit));\n const res = await this.authed(`/v1/feedback/strings?${qs}`, {\n method: \"GET\",\n });\n if (!res.ok) throw await this.problem(res, \"failed to load strings\");\n return (await res.json()) as StringsResponse;\n }\n\n /** Queue a rating; flushed on debounce or when the batch fills. */\n rate(payload: RatingInput): void {\n this.enqueue({ kind: \"rating\", payload });\n }\n\n /** Queue a suggestion; flushed on debounce or when the batch fills. */\n suggest(payload: SuggestionInput): void {\n this.enqueue({ kind: \"suggestion\", payload });\n }\n\n private enqueue(item: QueueItem): void {\n this.queue.push(item);\n if (this.queue.length >= this.cfg.maxBatch) {\n void this.flush();\n return;\n }\n if (this.timer) clearTimeout(this.timer);\n this.timer = setTimeout(() => void this.flush(), this.cfg.flushDebounceMs);\n }\n\n /**\n * Flush queued items. Best-effort: a transport/auth failure re-queues the\n * batch once and swallows the error so the host app never sees a throw.\n */\n async flush(): Promise<void> {\n if (this.timer) {\n clearTimeout(this.timer);\n this.timer = null;\n }\n if (!this.queue.length) return;\n const batch = this.queue;\n this.queue = [];\n const ratings = batch\n .filter((b) => b.kind === \"rating\")\n .map((b) => b.payload as RatingInput);\n const suggestions = batch\n .filter((b) => b.kind === \"suggestion\")\n .map((b) => b.payload as SuggestionInput);\n try {\n if (ratings.length) {\n await this.postBatch(\"/v1/feedback/ratings\", { ratings });\n }\n if (suggestions.length) {\n await this.postBatch(\"/v1/feedback/suggestions\", { suggestions });\n }\n } catch {\n // Re-queue once; feedback must never break the host app.\n this.queue.unshift(...batch);\n }\n }\n\n private async postBatch(\n path: string,\n body: unknown,\n ): Promise<BatchResponse> {\n const res = await this.authed(path, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", \"X-SDK\": `${SDK_LIB}@${SDK_VER}` },\n body: JSON.stringify(body),\n });\n if (!res.ok) throw await this.problem(res, \"batch post failed\");\n return (await res.json()) as BatchResponse;\n }\n\n private async problem(res: Response, fallback: string): Promise<FeedbackError> {\n let code: string | undefined;\n let detail = fallback;\n try {\n const body = (await res.json()) as { code?: string; detail?: string };\n code = body.code;\n if (body.detail) detail = body.detail;\n } catch {\n /* non-JSON body */\n }\n return new FeedbackError(detail, res.status, code);\n }\n}\n","/**\n * On-screen key discovery.\n *\n * Preferred source: the `@verbumia/*-i18n` SDK exposes a lightweight key\n * registry of keys it has rendered. When that registry is present on the\n * global (the i18n SDK publishes `globalThis.__verbumia_key_registry__`),\n * we read the keys touched on the current view. Otherwise the host app\n * passes an explicit `keys` list — the always-available fallback.\n *\n * The registry contract is intentionally tiny so any framework port of the\n * i18n SDK can implement it without depending on this package.\n */\n\nimport type { DeclaredKey } from \"./types\";\n\nconst REGISTRY_GLOBAL = \"__verbumia_key_registry__\";\n\ninterface KeyRegistry {\n /** Returns the keys rendered since the last `reset()` (or ever). */\n snapshot(): DeclaredKey[];\n reset?(): void;\n}\n\nfunction getRegistry(): KeyRegistry | null {\n const g = globalThis as Record<string, unknown>;\n const reg = g[REGISTRY_GLOBAL];\n if (\n reg &&\n typeof (reg as KeyRegistry).snapshot === \"function\"\n ) {\n return reg as KeyRegistry;\n }\n return null;\n}\n\n/** True when a compatible `@verbumia/*-i18n` registry is detectable. */\nexport function hasKeyRegistry(): boolean {\n return getRegistry() !== null;\n}\n\n/**\n * Resolve the on-screen keys: explicit `keys` prop always wins (it is the\n * customer's authoritative declaration); otherwise fall back to the i18n\n * registry snapshot; otherwise an empty list (widget shows \"no strings\").\n */\nexport function resolveKeys(explicit?: DeclaredKey[]): DeclaredKey[] {\n if (explicit && explicit.length) return dedupe(explicit);\n const reg = getRegistry();\n if (reg) return dedupe(reg.snapshot());\n return [];\n}\n\nfunction dedupe(keys: DeclaredKey[]): DeclaredKey[] {\n const seen = new Set<string>();\n const out: DeclaredKey[] = [];\n for (const k of keys) {\n const id = `${k.namespace}:${k.key}`;\n if (!seen.has(id)) {\n seen.add(id);\n out.push(k);\n }\n }\n return out;\n}\n"],"mappings":"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","names":[]}
|
package/dist/svelte/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
FeedbackClient,
|
|
3
3
|
FeedbackError,
|
|
4
4
|
resolveKeys
|
|
5
|
-
} from "../chunk-
|
|
5
|
+
} from "../chunk-NPUPBKOW.js";
|
|
6
6
|
|
|
7
7
|
// src/svelte/index.ts
|
|
8
8
|
import { writable } from "svelte/store";
|
|
@@ -19,9 +19,11 @@ function createFeedback(config) {
|
|
|
19
19
|
async function loadStrings() {
|
|
20
20
|
if (!client.hasConsented) await client.acceptTos();
|
|
21
21
|
const resolved = resolveKeys(config.keys);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
if (!resolved.length) {
|
|
23
|
+
strings.set([]);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const r = await client.getStrings({ keys: resolved });
|
|
25
27
|
strings.set(r.strings);
|
|
26
28
|
}
|
|
27
29
|
return {
|