@verbumia/feedback 0.2.6 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-EKU6NNWI.js +40 -0
- package/dist/chunk-EKU6NNWI.js.map +1 -0
- package/dist/{chunk-5RTFWOGT.js → chunk-EMGN6MR4.js} +87 -3
- package/dist/chunk-EMGN6MR4.js.map +1 -0
- package/dist/{client-qgDSbz3A.d.cts → client-CnEK_2SD.d.cts} +114 -1
- package/dist/{client-qgDSbz3A.d.ts → client-CnEK_2SD.d.ts} +114 -1
- package/dist/core/index.cjs +86 -2
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +2 -2
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +1 -1
- package/dist/{keys-BhuK_fy1.d.cts → keys-2_T5bDpX.d.cts} +1 -1
- package/dist/{keys-CEWu0Htb.d.ts → keys-eHc_lx5v.d.ts} +1 -1
- package/dist/native/index.cjs +317 -48
- package/dist/native/index.cjs.map +1 -1
- package/dist/native/index.d.cts +48 -4
- package/dist/native/index.d.ts +48 -4
- package/dist/native/index.js +200 -48
- package/dist/native/index.js.map +1 -1
- package/dist/react/index.cjs +319 -41
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +119 -4
- package/dist/react/index.d.ts +119 -4
- package/dist/react/index.js +202 -41
- package/dist/react/index.js.map +1 -1
- package/dist/svelte/index.cjs +86 -2
- package/dist/svelte/index.cjs.map +1 -1
- package/dist/svelte/index.d.cts +2 -2
- package/dist/svelte/index.d.ts +2 -2
- package/dist/svelte/index.js +1 -1
- package/dist/vue/index.cjs +86 -2
- package/dist/vue/index.cjs.map +1 -1
- package/dist/vue/index.d.cts +2 -2
- package/dist/vue/index.d.ts +2 -2
- package/dist/vue/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-5RTFWOGT.js.map +0 -1
package/dist/native/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {
|
|
2
|
+
t
|
|
3
|
+
} from "../chunk-EKU6NNWI.js";
|
|
1
4
|
import "../chunk-5NA2TFPG.js";
|
|
2
5
|
import {
|
|
3
6
|
FeedbackClient,
|
|
@@ -5,7 +8,7 @@ import {
|
|
|
5
8
|
__require,
|
|
6
9
|
hasKeyRegistry,
|
|
7
10
|
resolveKeys
|
|
8
|
-
} from "../chunk-
|
|
11
|
+
} from "../chunk-EMGN6MR4.js";
|
|
9
12
|
|
|
10
13
|
// src/native/plugin.tsx
|
|
11
14
|
import { useSyncExternalStore } from "react";
|
|
@@ -22,7 +25,7 @@ import {
|
|
|
22
25
|
TouchableOpacity,
|
|
23
26
|
View
|
|
24
27
|
} from "react-native";
|
|
25
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
28
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
26
29
|
var SafeAreaView;
|
|
27
30
|
try {
|
|
28
31
|
const mod = __require("react-native-safe-area-context");
|
|
@@ -44,11 +47,14 @@ var C = {
|
|
|
44
47
|
emeraldSoft: "#34d399"
|
|
45
48
|
};
|
|
46
49
|
function FeedbackModal(props) {
|
|
47
|
-
const { client, visible, keys, namespace, onClose } = props;
|
|
48
|
-
const [consented, setConsented] = useState(
|
|
50
|
+
const { client, visible, keys, namespace, tos = "modal", onClose } = props;
|
|
51
|
+
const [consented, setConsented] = useState(
|
|
52
|
+
tos === "skip" ? true : client.hasConsented
|
|
53
|
+
);
|
|
49
54
|
const [busy, setBusy] = useState(false);
|
|
50
55
|
const [error, setError] = useState(null);
|
|
51
56
|
const [strings, setStrings] = useState([]);
|
|
57
|
+
const [showSource, setShowSource] = useState(false);
|
|
52
58
|
const loadStrings = useCallback(async () => {
|
|
53
59
|
setBusy(true);
|
|
54
60
|
setError(null);
|
|
@@ -146,14 +152,49 @@ function FeedbackModal(props) {
|
|
|
146
152
|
// to its intrinsic height and the panel left a large empty
|
|
147
153
|
// area below the last row (the second half of the
|
|
148
154
|
// SeedSower repro).
|
|
149
|
-
/* @__PURE__ */ jsx(ScrollView, { style: { flex: 1 }, children: !strings.length ? /* @__PURE__ */ jsx(Text, { style: { color: C.dim }, children: busy ? "Loading\u2026" : "No strings to review on this view." }) :
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
/* @__PURE__ */ jsx(ScrollView, { style: { flex: 1 }, children: !strings.length ? /* @__PURE__ */ jsx(Text, { style: { color: C.dim }, children: busy ? "Loading\u2026" : "No strings to review on this view." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
156
|
+
/* @__PURE__ */ jsxs(
|
|
157
|
+
TouchableOpacity,
|
|
158
|
+
{
|
|
159
|
+
onPress: () => setShowSource((v) => !v),
|
|
160
|
+
accessibilityLabel: t(client.language, "showOriginal"),
|
|
161
|
+
style: {
|
|
162
|
+
alignSelf: "flex-end",
|
|
163
|
+
flexDirection: "row",
|
|
164
|
+
alignItems: "center",
|
|
165
|
+
marginBottom: 10,
|
|
166
|
+
paddingVertical: 4,
|
|
167
|
+
paddingHorizontal: 8,
|
|
168
|
+
borderWidth: 1,
|
|
169
|
+
borderColor: C.border,
|
|
170
|
+
borderRadius: 6
|
|
171
|
+
},
|
|
172
|
+
children: [
|
|
173
|
+
/* @__PURE__ */ jsx(
|
|
174
|
+
Text,
|
|
175
|
+
{
|
|
176
|
+
style: {
|
|
177
|
+
color: showSource ? C.emeraldSoft : C.dim,
|
|
178
|
+
fontSize: 12,
|
|
179
|
+
marginRight: 6
|
|
180
|
+
},
|
|
181
|
+
children: showSource ? "\u2611" : "\u2610"
|
|
182
|
+
}
|
|
183
|
+
),
|
|
184
|
+
/* @__PURE__ */ jsx(Text, { style: { color: C.dim, fontSize: 12 }, children: t(client.language, "showOriginal") })
|
|
185
|
+
]
|
|
186
|
+
}
|
|
187
|
+
),
|
|
188
|
+
strings.map((s) => /* @__PURE__ */ jsx(
|
|
189
|
+
StringRow,
|
|
190
|
+
{
|
|
191
|
+
s,
|
|
192
|
+
client,
|
|
193
|
+
showSource
|
|
194
|
+
},
|
|
195
|
+
`${s.namespace}:${s.key}`
|
|
196
|
+
))
|
|
197
|
+
] }) })
|
|
157
198
|
)
|
|
158
199
|
] })
|
|
159
200
|
}
|
|
@@ -164,11 +205,42 @@ function FeedbackModal(props) {
|
|
|
164
205
|
);
|
|
165
206
|
}
|
|
166
207
|
function StringRow(props) {
|
|
167
|
-
const { s, client } = props;
|
|
208
|
+
const { s, client, showSource } = props;
|
|
168
209
|
const [mine, setMine] = useState(s.my_rating);
|
|
169
210
|
const [show, setShow] = useState(false);
|
|
170
|
-
const
|
|
211
|
+
const mySuggestionId = s.my_suggestion ? s.my_suggestion.id : null;
|
|
212
|
+
const [text, setText] = useState(s.my_suggestion?.text ?? "");
|
|
171
213
|
const [sent, setSent] = useState(false);
|
|
214
|
+
const [editError, setEditError] = useState(null);
|
|
215
|
+
const submit = async () => {
|
|
216
|
+
const trimmed = text.trim();
|
|
217
|
+
if (!trimmed) return;
|
|
218
|
+
setEditError(null);
|
|
219
|
+
try {
|
|
220
|
+
if (mySuggestionId) {
|
|
221
|
+
await client.editSuggestion(mySuggestionId, trimmed);
|
|
222
|
+
} else {
|
|
223
|
+
client.suggest({
|
|
224
|
+
namespace: s.namespace,
|
|
225
|
+
key: s.key,
|
|
226
|
+
language: client.language,
|
|
227
|
+
translation_hash: s.translation_hash,
|
|
228
|
+
suggested_text: trimmed
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
setSent(true);
|
|
232
|
+
setShow(false);
|
|
233
|
+
} catch (e) {
|
|
234
|
+
setEditError(
|
|
235
|
+
e instanceof Error ? e.message : "Could not update the suggestion"
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
const showSourceRow = showSource && s.source_locale !== client.language;
|
|
240
|
+
const submitLabel = t(
|
|
241
|
+
client.language,
|
|
242
|
+
mySuggestionId ? "updateMySuggestion" : "submitSuggestion"
|
|
243
|
+
);
|
|
172
244
|
return /* @__PURE__ */ jsxs(
|
|
173
245
|
View,
|
|
174
246
|
{
|
|
@@ -186,6 +258,19 @@ function StringRow(props) {
|
|
|
186
258
|
" \xB7 ",
|
|
187
259
|
s.key
|
|
188
260
|
] }),
|
|
261
|
+
showSourceRow ? /* @__PURE__ */ jsx(
|
|
262
|
+
Text,
|
|
263
|
+
{
|
|
264
|
+
accessibilityLabel: "source",
|
|
265
|
+
style: {
|
|
266
|
+
color: C.dim,
|
|
267
|
+
fontSize: 12,
|
|
268
|
+
fontStyle: "italic",
|
|
269
|
+
marginTop: 4
|
|
270
|
+
},
|
|
271
|
+
children: s.source_text
|
|
272
|
+
}
|
|
273
|
+
) : null,
|
|
189
274
|
/* @__PURE__ */ jsx(Text, { style: { color: C.text, fontSize: 15, marginVertical: 6 }, children: s.value }),
|
|
190
275
|
/* @__PURE__ */ jsxs(View, { style: { flexDirection: "row" }, children: [
|
|
191
276
|
[1, 2, 3, 4, 5].map((n) => /* @__PURE__ */ jsx(
|
|
@@ -221,7 +306,7 @@ function StringRow(props) {
|
|
|
221
306
|
{
|
|
222
307
|
onPress: () => setShow(!show),
|
|
223
308
|
style: { marginLeft: "auto" },
|
|
224
|
-
children: /* @__PURE__ */ jsx(Text, { style: { color: C.emeraldSoft }, children: sent ? "Suggested \u2713" :
|
|
309
|
+
children: /* @__PURE__ */ jsx(Text, { style: { color: C.emeraldSoft }, children: sent ? "Suggested \u2713" : submitLabel })
|
|
225
310
|
}
|
|
226
311
|
)
|
|
227
312
|
] }),
|
|
@@ -245,29 +330,18 @@ function StringRow(props) {
|
|
|
245
330
|
}
|
|
246
331
|
}
|
|
247
332
|
),
|
|
333
|
+
editError ? /* @__PURE__ */ jsx(Text, { style: { color: "#f87171", fontSize: 12, marginTop: 6 }, children: editError }) : null,
|
|
248
334
|
/* @__PURE__ */ jsx(
|
|
249
335
|
TouchableOpacity,
|
|
250
336
|
{
|
|
251
|
-
onPress: () =>
|
|
252
|
-
if (!text.trim()) return;
|
|
253
|
-
client.suggest({
|
|
254
|
-
namespace: s.namespace,
|
|
255
|
-
key: s.key,
|
|
256
|
-
language: client.language,
|
|
257
|
-
translation_hash: s.translation_hash,
|
|
258
|
-
suggested_text: text.trim()
|
|
259
|
-
});
|
|
260
|
-
setSent(true);
|
|
261
|
-
setShow(false);
|
|
262
|
-
setText("");
|
|
263
|
-
},
|
|
337
|
+
onPress: () => void submit(),
|
|
264
338
|
style: {
|
|
265
339
|
marginTop: 6,
|
|
266
340
|
backgroundColor: C.emerald,
|
|
267
341
|
borderRadius: 6,
|
|
268
342
|
padding: 10
|
|
269
343
|
},
|
|
270
|
-
children: /* @__PURE__ */ jsx(Text, { style: { color: "#03110c", fontWeight: "700", textAlign: "center" }, children:
|
|
344
|
+
children: /* @__PURE__ */ jsx(Text, { style: { color: "#03110c", fontWeight: "700", textAlign: "center" }, children: submitLabel })
|
|
271
345
|
}
|
|
272
346
|
)
|
|
273
347
|
] }) : null
|
|
@@ -279,42 +353,73 @@ function StringRow(props) {
|
|
|
279
353
|
// src/native/plugin.tsx
|
|
280
354
|
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
281
355
|
function makeStore() {
|
|
282
|
-
let
|
|
356
|
+
let state = { isOpen: false, snapshotKeys: void 0 };
|
|
283
357
|
const listeners = /* @__PURE__ */ new Set();
|
|
284
358
|
return {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
open
|
|
289
|
-
|
|
359
|
+
getState: () => state,
|
|
360
|
+
setOpen(open, snapshotKeys) {
|
|
361
|
+
const next = {
|
|
362
|
+
isOpen: open,
|
|
363
|
+
snapshotKeys: open ? snapshotKeys : void 0
|
|
364
|
+
};
|
|
365
|
+
if (state.isOpen === next.isOpen && state.snapshotKeys === next.snapshotKeys) {
|
|
366
|
+
return;
|
|
290
367
|
}
|
|
368
|
+
state = next;
|
|
369
|
+
listeners.forEach((l) => l());
|
|
291
370
|
},
|
|
292
371
|
subscribe(l) {
|
|
293
372
|
listeners.add(l);
|
|
294
|
-
return () =>
|
|
373
|
+
return () => {
|
|
374
|
+
listeners.delete(l);
|
|
375
|
+
};
|
|
295
376
|
}
|
|
296
377
|
};
|
|
297
378
|
}
|
|
379
|
+
async function captureCurrentViewSnapshot(i18next) {
|
|
380
|
+
const reg = globalThis.__verbumia_key_registry__;
|
|
381
|
+
if (!reg) return [];
|
|
382
|
+
const lng = i18next?.language;
|
|
383
|
+
const change = i18next?.changeLanguage;
|
|
384
|
+
if (typeof change === "function" && typeof lng === "string" && lng) {
|
|
385
|
+
reg.reset?.();
|
|
386
|
+
try {
|
|
387
|
+
await change(lng);
|
|
388
|
+
} catch {
|
|
389
|
+
}
|
|
390
|
+
await new Promise((r) => {
|
|
391
|
+
if (typeof requestAnimationFrame === "function") {
|
|
392
|
+
requestAnimationFrame(() => r());
|
|
393
|
+
} else {
|
|
394
|
+
setTimeout(() => r(), 16);
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
return reg.snapshot();
|
|
398
|
+
}
|
|
399
|
+
return reg.snapshot();
|
|
400
|
+
}
|
|
298
401
|
function feedbackPlugin(options) {
|
|
299
402
|
const store = makeStore();
|
|
300
403
|
let client = null;
|
|
301
404
|
function Outlet() {
|
|
302
|
-
const
|
|
405
|
+
const state = useSyncExternalStore(
|
|
303
406
|
store.subscribe,
|
|
304
|
-
store.
|
|
305
|
-
store.
|
|
407
|
+
store.getState,
|
|
408
|
+
store.getState
|
|
306
409
|
);
|
|
307
410
|
if (!client) return null;
|
|
308
411
|
const c = client;
|
|
412
|
+
const panelKeys = options.keys ?? state.snapshotKeys;
|
|
309
413
|
return /* @__PURE__ */ jsx2(
|
|
310
414
|
FeedbackModal,
|
|
311
415
|
{
|
|
312
416
|
client: c,
|
|
313
|
-
visible: isOpen,
|
|
314
|
-
keys:
|
|
417
|
+
visible: state.isOpen,
|
|
418
|
+
keys: panelKeys,
|
|
315
419
|
namespace: options.namespace,
|
|
420
|
+
tos: options.tos ?? "modal",
|
|
316
421
|
onClose: () => {
|
|
317
|
-
store.
|
|
422
|
+
store.setOpen(false);
|
|
318
423
|
void c.flush();
|
|
319
424
|
}
|
|
320
425
|
}
|
|
@@ -324,24 +429,71 @@ function feedbackPlugin(options) {
|
|
|
324
429
|
name: "@verbumia/feedback",
|
|
325
430
|
setup(ctx) {
|
|
326
431
|
const initialLanguage = options.language ?? ctx.i18n?.language ?? ctx.config.defaultLocale;
|
|
432
|
+
const tos = options.tos ?? "modal";
|
|
327
433
|
client = new FeedbackClient({
|
|
328
434
|
apiBase: options.apiBase ?? ctx.config.apiBase ?? "https://api.verbumia.dev",
|
|
329
435
|
projectId: options.projectId ?? ctx.config.projectUuid,
|
|
330
436
|
language: initialLanguage,
|
|
331
437
|
endUserId: options.endUserId,
|
|
332
|
-
fetchImpl: options.fetchImpl
|
|
438
|
+
fetchImpl: options.fetchImpl,
|
|
439
|
+
apiKey: options.apiKey,
|
|
440
|
+
autoAcceptTos: tos !== "skip"
|
|
333
441
|
});
|
|
442
|
+
const clientRef = client;
|
|
443
|
+
let addonState = null;
|
|
444
|
+
const cta = options.cta ?? "auto";
|
|
445
|
+
const scope = options.scope ?? "current-view";
|
|
446
|
+
const ctxI18next = ctx.i18n?.i18next;
|
|
447
|
+
const refreshState = async () => {
|
|
448
|
+
try {
|
|
449
|
+
const next = await clientRef.getAddonState();
|
|
450
|
+
if (next !== null) addonState = next;
|
|
451
|
+
} catch {
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
if (options.apiKey) void refreshState();
|
|
334
455
|
let langUnsub;
|
|
335
456
|
if (typeof ctx.onLanguageChange === "function") {
|
|
336
|
-
langUnsub = ctx.onLanguageChange((lng) =>
|
|
457
|
+
langUnsub = ctx.onLanguageChange((lng) => {
|
|
458
|
+
clientRef.setLanguage(lng);
|
|
459
|
+
void refreshState();
|
|
460
|
+
});
|
|
337
461
|
}
|
|
338
462
|
const controller = {
|
|
339
|
-
open: () =>
|
|
463
|
+
open: async () => {
|
|
464
|
+
if (scope === "current-view") {
|
|
465
|
+
const snapshot = await captureCurrentViewSnapshot(ctxI18next);
|
|
466
|
+
store.setOpen(true, snapshot);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
store.setOpen(true);
|
|
470
|
+
},
|
|
340
471
|
close: () => {
|
|
341
|
-
store.
|
|
342
|
-
void
|
|
472
|
+
store.setOpen(false);
|
|
473
|
+
void clientRef?.flush();
|
|
474
|
+
},
|
|
475
|
+
client: clientRef,
|
|
476
|
+
get tosVersion() {
|
|
477
|
+
return clientRef.tosVersion;
|
|
478
|
+
},
|
|
479
|
+
get hasAcceptedTos() {
|
|
480
|
+
return clientRef.hasAcceptedTos;
|
|
343
481
|
},
|
|
344
|
-
|
|
482
|
+
acceptTos: async () => {
|
|
483
|
+
await clientRef.acceptTos();
|
|
484
|
+
if (!options.apiKey) await refreshState();
|
|
485
|
+
},
|
|
486
|
+
get isActive() {
|
|
487
|
+
if (cta === "show") return true;
|
|
488
|
+
if (cta === "hide") return false;
|
|
489
|
+
return addonState ? addonState.isActive : null;
|
|
490
|
+
},
|
|
491
|
+
get enabledLanguages() {
|
|
492
|
+
return addonState ? addonState.enabledLanguages : null;
|
|
493
|
+
},
|
|
494
|
+
get sku() {
|
|
495
|
+
return addonState ? addonState.sku : null;
|
|
496
|
+
}
|
|
345
497
|
};
|
|
346
498
|
options.onReady?.(controller);
|
|
347
499
|
if (options.controllerRef) options.controllerRef.current = controller;
|
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 i18n?: { language?: string };\n config: {\n apiBase?: string;\n projectUuid: string;\n defaultLocale: string;\n };\n /** #806 — subscribe to runtime language changes (see\n * VerbumiaPluginContext in `@verbumia/react-i18next` ≥1.0.5). Returns\n * an unsubscribe fn the plugin MUST call from teardown. Optional so a\n * pre-1.0.5 react-i18next falls back gracefully. */\n onLanguageChange?: (cb: (lng: string) => void) => () => void;\n}\nexport interface I18nPlugin {\n name: string;\n setup?: (ctx: I18nPluginContext) => void | (() => void);\n render?: () => ReactNode;\n}\n\nexport interface FeedbackController {\n 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 /** Optional namespace filter (CONTRACT v6, additive). Single ns or\n * list; applied AFTER rendered-scoping (`rendered ∩ namespace`).\n * Unset ⇒ all resolved keys (v5 behaviour). */\n namespace?: string | string[];\n onReady?: (controller: FeedbackController) => void;\n controllerRef?: { current: FeedbackController | null };\n fetchImpl?: typeof fetch;\n}\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 namespace={options.namespace}\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 // #806 SeedSower lang-change init source — same precedence as the\n // /react plugin: explicit option, then current i18n language,\n // then boot defaultLocale.\n const initialLanguage =\n options.language ?? ctx.i18n?.language ?? ctx.config.defaultLocale;\n client = new FeedbackClient({\n apiBase:\n options.apiBase ?? ctx.config.apiBase ?? \"https://api.verbumia.dev\",\n projectId: options.projectId ?? ctx.config.projectUuid,\n language: initialLanguage,\n endUserId: options.endUserId,\n fetchImpl: options.fetchImpl,\n });\n // #806 — re-sync the client on runtime language changes. Optional;\n // missing field on a non-Verbumia host falls back gracefully.\n let langUnsub: (() => void) | undefined;\n if (typeof ctx.onLanguageChange === \"function\") {\n langUnsub = ctx.onLanguageChange((lng) => client?.setLanguage(lng));\n }\n const controller: FeedbackController = {\n open: () => store.set(true),\n close: () => {\n store.set(false);\n void client?.flush();\n },\n client,\n };\n options.onReady?.(controller);\n if (options.controllerRef) options.controllerRef.current = controller;\n return () => {\n langUnsub?.();\n if (options.controllerRef) options.controllerRef.current = null;\n void client?.flush();\n };\n },\n render: () => <Outlet />,\n };\n}\n","import { createElement, useCallback, useEffect, useState, type ComponentType, type ReactNode } from \"react\";\nimport {\n KeyboardAvoidingView,\n Modal,\n Platform,\n ScrollView,\n Text,\n TextInput,\n TouchableOpacity,\n View,\n} from \"react-native\";\n\nimport type { FeedbackClient } from \"../core/client\";\nimport { resolveKeys } from \"../core/keys\";\nimport type { DeclaredKey, FeedbackString } from \"../core/types\";\n\n/**\n * Soft-resolve `react-native-safe-area-context`'s `SafeAreaView` (#806\n * SeedSower Android panel-layout bug). When the host installed it (Expo\n * apps ship it by default; bare RN apps that follow the standard setup do\n * too), the panel's bottom edge respects the system gesture bar / home\n * indicator. When it isn't installed (web bundles, RN apps that opted\n * out), fall back to a plain `View` — same children, same style, no\n * insets. The dependency is listed as an OPTIONAL `peerDependency` so the\n * npm install never warns on web-only consumers.\n */\ntype SafeAreaProps = { edges?: ReadonlyArray<string>; style?: unknown; children?: ReactNode };\n// Local `require` declaration so we don't pull @types/node into this\n// package. Metro / Node both inject `require` at module scope; in the\n// ESM build path tsup polyfills it via createRequire(import.meta.url).\ndeclare const require: (id: string) => unknown;\nlet SafeAreaView: ComponentType<SafeAreaProps>;\ntry {\n const mod = require(\"react-native-safe-area-context\") as {\n SafeAreaView: ComponentType<SafeAreaProps>;\n };\n SafeAreaView = mod.SafeAreaView;\n} catch {\n SafeAreaView = ({ children, style }: SafeAreaProps) =>\n createElement(\n View as ComponentType<{ style?: unknown; children?: ReactNode }>,\n { style },\n children,\n );\n}\n\nconst C = {\n bg: \"#0b0f0e\",\n panel: \"#111714\",\n border: \"#1f2a25\",\n text: \"#e7f5ef\",\n dim: \"#8aa79b\",\n emerald: \"#10b981\",\n emeraldSoft: \"#34d399\",\n};\n\nexport function FeedbackModal(props: {\n client: FeedbackClient;\n visible: boolean;\n keys?: DeclaredKey[];\n namespace?: string | string[];\n onClose: () => void;\n}) {\n const { client, visible, keys, namespace, onClose } = props;\n const [consented, setConsented] = useState(client.hasConsented);\n const [busy, setBusy] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [strings, setStrings] = useState<FeedbackString[]>([]);\n\n const loadStrings = useCallback(async () => {\n setBusy(true);\n setError(null);\n try {\n // On-screen scoping is NORMATIVE (spec ltm 373, CONTRACT v5): show\n // ONLY rendered keys. Empty -> \"no strings on this view\"; never\n // fall back to fetching the whole project.\n const resolved = resolveKeys(keys, namespace);\n if (!resolved.length) {\n // #806 SeedSower P1 — same diagnostic as the web panel: an empty\n // registry resolve with no explicit `keys` prop almost always\n // means the host's `useTranslation` imports went to\n // `react-i18next` instead of `@verbumia/react-i18next`.\n if (!keys || keys.length === 0) {\n // eslint-disable-next-line no-console\n console.warn(\n \"[verbumia/feedback] registry empty — are your useTranslation imports coming from @verbumia/react-i18next?\",\n );\n }\n setStrings([]);\n return;\n }\n const res = await client.getStrings({ keys: resolved });\n setStrings(res.strings);\n } catch (e) {\n setError(e instanceof Error ? e.message : \"Failed to load strings\");\n } finally {\n setBusy(false);\n }\n }, [client, keys, namespace]);\n\n useEffect(() => {\n if (visible && consented) void loadStrings();\n }, [visible, consented, loadStrings]);\n\n const accept = useCallback(async () => {\n setBusy(true);\n try {\n await client.acceptTos();\n setConsented(true);\n } catch (e) {\n setError(e instanceof Error ? e.message : \"Could not accept the terms\");\n } finally {\n setBusy(false);\n }\n }, [client]);\n\n return (\n <Modal\n visible={visible}\n transparent\n animationType=\"slide\"\n onRequestClose={onClose}\n >\n {/*\n #806 SeedSower native panel layout — KeyboardAvoidingView wraps the\n whole overlay so the suggestion TextInput isn't covered by the soft\n keyboard. `padding` on iOS / `height` on Android matches the RN\n community convention; flex:1 keeps the overlay full-screen above\n the keyboard reservation.\n */}\n <KeyboardAvoidingView\n style={{ flex: 1 }}\n behavior={Platform.OS === \"ios\" ? \"padding\" : \"height\"}\n >\n <View style={{ flex: 1, backgroundColor: \"rgba(0,0,0,.55)\", justifyContent: \"flex-end\" }}>\n {/*\n Panel sizing — `height:'85%'` gives a CONCRETE 85% of the\n modal's parent (the full-screen overlay), which is what the\n ToS step / strings ScrollView need to flex against. The prior\n `maxHeight:'85%'` was a ceiling only: without a height/flex\n child, the panel collapsed to its ToS intrinsic content (~15%\n of screen) on RN 0.77.3 Android. `maxHeight` is kept as a\n backstop in case a future child wants to render shorter (it\n never exceeds 85%; it can render shorter if the content does).\n\n SafeAreaView edges={['bottom']} keeps the CTA off the Android\n gesture bar / iOS home indicator. Falls back to plain View\n when react-native-safe-area-context isn't installed.\n */}\n <SafeAreaView\n edges={[\"bottom\"]}\n style={{\n height: \"85%\",\n maxHeight: \"85%\",\n backgroundColor: C.bg,\n borderTopWidth: 1,\n borderColor: C.border,\n }}\n >\n <View style={{ flex: 1, padding: 18 }}>\n <View\n style={{\n flexDirection: \"row\",\n justifyContent: \"space-between\",\n marginBottom: 12,\n }}\n >\n <Text style={{ color: C.emeraldSoft, fontWeight: \"700\", fontSize: 16 }}>\n Translation feedback\n </Text>\n <TouchableOpacity onPress={onClose} accessibilityLabel=\"Close\">\n <Text style={{ color: C.dim, fontSize: 20 }}>×</Text>\n </TouchableOpacity>\n </View>\n\n {error ? (\n <Text style={{ color: \"#f87171\", marginBottom: 8 }}>{error}</Text>\n ) : null}\n\n {!consented ? (\n <View>\n <Text style={{ color: C.text, lineHeight: 21 }}>\n Help improve the translations in this app. Your ratings and\n suggestions are sent to the app owner via Verbumia. Don’t\n submit personal or sensitive information.\n </Text>\n <TouchableOpacity\n disabled={busy}\n onPress={accept}\n style={{\n marginTop: 14,\n backgroundColor: C.emerald,\n borderRadius: 8,\n padding: 14,\n }}\n >\n <Text style={{ color: \"#03110c\", fontWeight: \"700\", textAlign: \"center\" }}>\n {busy ? \"…\" : `I accept the terms (v${client.tosVersion})`}\n </Text>\n </TouchableOpacity>\n </View>\n ) : (\n // flex:1 makes the ScrollView fill the remaining panel\n // height after the header; without it the list collapsed\n // to its intrinsic height and the panel left a large empty\n // area below the last row (the second half of the\n // SeedSower repro).\n <ScrollView style={{ flex: 1 }}>\n {!strings.length ? (\n <Text style={{ color: C.dim }}>\n {busy ? \"Loading…\" : \"No strings to review on this view.\"}\n </Text>\n ) : (\n strings.map((s) => (\n <StringRow\n key={`${s.namespace}:${s.key}`}\n s={s}\n client={client}\n />\n ))\n )}\n </ScrollView>\n )}\n </View>\n </SafeAreaView>\n </View>\n </KeyboardAvoidingView>\n </Modal>\n );\n}\n\nfunction StringRow(props: { s: FeedbackString; client: FeedbackClient }) {\n const { s, client } = props;\n const [mine, setMine] = useState<number | null>(s.my_rating);\n const [show, setShow] = useState(false);\n const [text, setText] = useState(\"\");\n const [sent, setSent] = useState(false);\n\n return (\n <View\n style={{\n backgroundColor: C.panel,\n borderWidth: 1,\n borderColor: C.border,\n borderRadius: 10,\n padding: 12,\n marginBottom: 10,\n }}\n >\n <Text style={{ color: C.dim, fontSize: 12 }}>\n {s.namespace} · {s.key}\n </Text>\n <Text style={{ color: C.text, fontSize: 15, marginVertical: 6 }}>\n {s.value}\n </Text>\n <View style={{ flexDirection: \"row\" }}>\n {[1, 2, 3, 4, 5].map((n) => (\n <TouchableOpacity\n key={n}\n accessibilityLabel={`${n} stars`}\n onPress={() => {\n setMine(n);\n client.rate({\n namespace: s.namespace,\n key: s.key,\n language: client.language,\n translation_hash: s.translation_hash,\n stars: n,\n });\n }}\n >\n <Text\n style={{\n fontSize: 22,\n marginRight: 4,\n color: mine && n <= mine ? C.emeraldSoft : C.border,\n }}\n >\n ★\n </Text>\n </TouchableOpacity>\n ))}\n <TouchableOpacity\n onPress={() => setShow(!show)}\n style={{ marginLeft: \"auto\" }}\n >\n <Text style={{ color: C.emeraldSoft }}>\n {sent ? \"Suggested ✓\" : \"Suggest\"}\n </Text>\n </TouchableOpacity>\n </View>\n {show ? (\n <View style={{ marginTop: 10 }}>\n <TextInput\n value={text}\n onChangeText={setText}\n multiline\n numberOfLines={3}\n placeholder=\"Your suggested translation…\"\n placeholderTextColor={C.dim}\n style={{\n backgroundColor: C.bg,\n color: C.text,\n borderWidth: 1,\n borderColor: C.border,\n borderRadius: 6,\n padding: 8,\n }}\n />\n <TouchableOpacity\n onPress={() => {\n if (!text.trim()) return;\n client.suggest({\n namespace: s.namespace,\n key: s.key,\n language: client.language,\n translation_hash: s.translation_hash,\n suggested_text: text.trim(),\n });\n setSent(true);\n setShow(false);\n setText(\"\");\n }}\n style={{\n marginTop: 6,\n backgroundColor: C.emerald,\n borderRadius: 6,\n padding: 10,\n }}\n >\n <Text style={{ color: \"#03110c\", fontWeight: \"700\", textAlign: \"center\" }}>\n Send suggestion\n </Text>\n </TouchableOpacity>\n </View>\n ) : null}\n </View>\n );\n}\n"],"mappings":"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","names":["jsx"]}
|
|
1
|
+
{"version":3,"sources":["../../src/native/plugin.tsx","../../src/native/panel.tsx"],"sourcesContent":["/**\n * `@verbumia/feedback` (React Native / Expo) as a PLUGIN of the\n * `@verbumia/*-i18n` provider — same architecture as the web plugin\n * (task 599): no second context; isolated sibling outlet; imperative\n * controller for the host CTA; sessionId is server-minted. Native uses\n * an RN <Modal> (its own overlay) toggled by a private store, so the\n * host app never re-renders when feedback opens.\n */\nimport { useSyncExternalStore, type ReactNode } from \"react\";\n\nimport { FeedbackClient } from \"../core/client\";\nimport type { DeclaredKey, FeedbackAddonState } from \"../core/types\";\nimport { FeedbackModal } from \"./panel\";\n\nexport interface I18nPluginContext {\n i18n?: {\n language?: string;\n /** 0.2.7 — direct i18next handle used by the `scope: \"current-view\"`\n * snapshot path (see /react plugin for full notes). Optional. */\n i18next?: {\n language?: string;\n changeLanguage?: (lng: string) => Promise<unknown>;\n };\n };\n config: {\n apiBase?: string;\n projectUuid: string;\n defaultLocale: string;\n };\n /** #806 — subscribe to runtime language changes (see\n * VerbumiaPluginContext in `@verbumia/react-i18next` ≥1.0.5). Returns\n * an unsubscribe fn the plugin MUST call from teardown. Optional so a\n * pre-1.0.5 react-i18next falls back gracefully. */\n onLanguageChange?: (cb: (lng: string) => void) => () => void;\n}\nexport interface I18nPlugin {\n name: string;\n setup?: (ctx: I18nPluginContext) => void | (() => void);\n render?: () => ReactNode;\n}\n\nexport interface FeedbackController {\n /** From 0.2.7 `open()` is async (Promise<void>) so the\n * `scope: \"current-view\"` snapshot sequence can run before the modal\n * mounts. Hosts that don't await still work; the modal mounts when\n * the promise resolves. */\n open: () => Promise<void>;\n close: () => void;\n client: FeedbackClient;\n /** 0.2.7 — current ToS version. */\n readonly tosVersion: string;\n /** 0.2.7 — whether this end-user has a live session-token bundle. */\n readonly hasAcceptedTos: boolean;\n /** 0.2.7 — programmatically POST `/v1/feedback/tos` (idempotent). Used\n * by hosts that build their own ToS page (`tos: \"skip\"`). */\n acceptTos: () => Promise<void>;\n /** 0.2.7 — addon-state-derived flag (see /react plugin docs).\n * `null` before the first state fetch resolves. */\n readonly isActive: boolean | null;\n /** 0.2.7 — `state.enabledLanguages` (string[] | \"all\" | null). */\n readonly enabledLanguages: string[] | \"all\" | null;\n /** 0.2.7 — raw SKU code. */\n readonly sku: \"feedback_starter\" | \"feedback_unlimited\" | null;\n}\n\nexport interface FeedbackPluginOptions {\n // No `tosVersion` — SDK build-time constant (SDK_TOS_VERSION, task 616).\n language?: string;\n apiBase?: string;\n projectId?: string;\n endUserId?: string;\n keys?: DeclaredKey[];\n /** Optional namespace filter (CONTRACT v6, additive). Single ns or\n * list; applied AFTER rendered-scoping (`rendered ∩ namespace`).\n * Unset ⇒ all resolved keys (v5 behaviour). */\n namespace?: string | string[];\n onReady?: (controller: FeedbackController) => void;\n controllerRef?: { current: FeedbackController | null };\n fetchImpl?: typeof fetch;\n /** 0.2.7 — `\"modal\"` (default, BC) keeps the built-in ToS step;\n * `\"skip\"` removes it (host owns acceptance via\n * `controller.acceptTos()`); /strings 401s render via the existing\n * error state until acceptance lands. */\n tos?: \"modal\" | \"skip\";\n /** 0.2.7 — host's project API key (`vrb_live_…`, scope project:read);\n * enables addon-state fetch at setup time. */\n apiKey?: string;\n /** 0.2.7 — controls `controller.isActive` (see /react plugin docs). */\n cta?: \"auto\" | \"show\" | \"hide\";\n /** 0.2.7 — on-screen-key set the panel reads (see /react plugin\n * docs). Default `\"current-view\"` (controller.open() resets the\n * registry, force-emits a same-locale languageChanged, waits 1\n * frame, snapshots). `\"all\"` preserves the pre-0.2.7 accumulator\n * view. */\n scope?: \"current-view\" | \"all\";\n}\n\n/** 0.2.7 panel store — see /react plugin docs. */\ntype PanelState = {\n isOpen: boolean;\n snapshotKeys: DeclaredKey[] | undefined;\n};\nfunction makeStore() {\n let state: PanelState = { isOpen: false, snapshotKeys: undefined };\n const listeners = new Set<() => void>();\n return {\n getState: (): PanelState => state,\n setOpen(open: boolean, snapshotKeys?: DeclaredKey[]): void {\n const next: PanelState = {\n isOpen: open,\n snapshotKeys: open ? snapshotKeys : undefined,\n };\n if (\n state.isOpen === next.isOpen &&\n state.snapshotKeys === next.snapshotKeys\n ) {\n return;\n }\n state = next;\n listeners.forEach((l) => l());\n },\n subscribe(l: () => void): () => void {\n listeners.add(l);\n return () => {\n listeners.delete(l);\n };\n },\n };\n}\n\n/** 0.2.7 — `scope: \"current-view\"` snapshot. See /react plugin for\n * the full rationale + known limitation. On native (Hermes) we yield\n * via `setTimeout(16)` since `requestAnimationFrame` is rarely\n * injected by Hermes runtimes. */\nasync function captureCurrentViewSnapshot(\n i18next:\n | {\n language?: string;\n changeLanguage?: (lng: string) => Promise<unknown>;\n }\n | undefined,\n): Promise<DeclaredKey[]> {\n type Reg = {\n snapshot: () => DeclaredKey[];\n reset?: () => void;\n };\n const reg = (globalThis as Record<string, unknown>)\n .__verbumia_key_registry__ as Reg | undefined;\n if (!reg) return [];\n const lng = i18next?.language;\n const change = i18next?.changeLanguage;\n if (typeof change === \"function\" && typeof lng === \"string\" && lng) {\n reg.reset?.();\n try {\n await change(lng);\n } catch {\n // best-effort\n }\n await new Promise<void>((r) => {\n if (typeof requestAnimationFrame === \"function\") {\n requestAnimationFrame(() => r());\n } else {\n setTimeout(() => r(), 16);\n }\n });\n return reg.snapshot();\n }\n return reg.snapshot();\n}\n\nexport function feedbackPlugin(options: FeedbackPluginOptions): I18nPlugin {\n const store = makeStore();\n let client: FeedbackClient | null = null;\n\n function Outlet() {\n const state = useSyncExternalStore(\n store.subscribe,\n store.getState,\n store.getState,\n );\n if (!client) return null;\n const c = client;\n // Precedence: options.keys > scope='current-view' snapshot > registry.\n const panelKeys = options.keys ?? state.snapshotKeys;\n return (\n <FeedbackModal\n client={c}\n visible={state.isOpen}\n keys={panelKeys}\n namespace={options.namespace}\n tos={options.tos ?? \"modal\"}\n onClose={() => {\n store.setOpen(false);\n void c.flush();\n }}\n />\n );\n }\n\n return {\n name: \"@verbumia/feedback\",\n setup(ctx) {\n // #806 SeedSower lang-change init source — same precedence as the\n // /react plugin: explicit option, then current i18n language,\n // then boot defaultLocale.\n const initialLanguage =\n options.language ?? ctx.i18n?.language ?? ctx.config.defaultLocale;\n // 0.2.7 ToS: `tos: \"skip\"` flips `autoAcceptTos: false` so the\n // client refuses to silently POST /v1/feedback/tos on the user's\n // behalf — the host owns acceptance via `controller.acceptTos()`.\n const tos: \"modal\" | \"skip\" = options.tos ?? \"modal\";\n client = new FeedbackClient({\n apiBase:\n options.apiBase ?? ctx.config.apiBase ?? \"https://api.verbumia.dev\",\n projectId: options.projectId ?? ctx.config.projectUuid,\n language: initialLanguage,\n endUserId: options.endUserId,\n fetchImpl: options.fetchImpl,\n apiKey: options.apiKey,\n autoAcceptTos: tos !== \"skip\",\n });\n const clientRef = client;\n // 0.2.7 addon-state cache — same semantics as the /react plugin.\n let addonState: FeedbackAddonState | null = null;\n const cta: \"auto\" | \"show\" | \"hide\" = options.cta ?? \"auto\";\n // 0.2.7 scope (default \"current-view\" — see plugin docs).\n const scope: \"current-view\" | \"all\" = options.scope ?? \"current-view\";\n const ctxI18next = ctx.i18n?.i18next;\n const refreshState = async (): Promise<void> => {\n try {\n const next = await clientRef.getAddonState();\n if (next !== null) addonState = next;\n } catch {\n // Pre-acceptTos + no apiKey path — defer until acceptTos().\n }\n };\n if (options.apiKey) void refreshState();\n // #806 — re-sync the client + refresh addon-state on lang change.\n let langUnsub: (() => void) | undefined;\n if (typeof ctx.onLanguageChange === \"function\") {\n langUnsub = ctx.onLanguageChange((lng) => {\n clientRef.setLanguage(lng);\n void refreshState();\n });\n }\n const controller: FeedbackController = {\n open: async (): Promise<void> => {\n if (scope === \"current-view\") {\n const snapshot = await captureCurrentViewSnapshot(ctxI18next);\n store.setOpen(true, snapshot);\n return;\n }\n store.setOpen(true);\n },\n close: () => {\n store.setOpen(false);\n void clientRef?.flush();\n },\n client: clientRef,\n get tosVersion(): string {\n return clientRef.tosVersion;\n },\n get hasAcceptedTos(): boolean {\n return clientRef.hasAcceptedTos;\n },\n acceptTos: async (): Promise<void> => {\n await clientRef.acceptTos();\n if (!options.apiKey) await refreshState();\n },\n get isActive(): boolean | null {\n if (cta === \"show\") return true;\n if (cta === \"hide\") return false;\n return addonState ? addonState.isActive : null;\n },\n get enabledLanguages(): string[] | \"all\" | null {\n return addonState ? addonState.enabledLanguages : null;\n },\n get sku(): \"feedback_starter\" | \"feedback_unlimited\" | null {\n return addonState ? addonState.sku : null;\n },\n };\n options.onReady?.(controller);\n if (options.controllerRef) options.controllerRef.current = controller;\n return () => {\n langUnsub?.();\n if (options.controllerRef) options.controllerRef.current = null;\n void client?.flush();\n };\n },\n render: () => <Outlet />,\n };\n}\n","import { createElement, useCallback, useEffect, useState, type ComponentType, type ReactNode } from \"react\";\nimport {\n KeyboardAvoidingView,\n Modal,\n Platform,\n ScrollView,\n Text,\n TextInput,\n TouchableOpacity,\n View,\n} from \"react-native\";\n\nimport type { FeedbackClient } from \"../core/client\";\nimport { resolveKeys } from \"../core/keys\";\nimport { t as tr } from \"../core/locales\";\nimport type { DeclaredKey, FeedbackString } from \"../core/types\";\n\n/**\n * Soft-resolve `react-native-safe-area-context`'s `SafeAreaView` (#806\n * SeedSower Android panel-layout bug). When the host installed it (Expo\n * apps ship it by default; bare RN apps that follow the standard setup do\n * too), the panel's bottom edge respects the system gesture bar / home\n * indicator. When it isn't installed (web bundles, RN apps that opted\n * out), fall back to a plain `View` — same children, same style, no\n * insets. The dependency is listed as an OPTIONAL `peerDependency` so the\n * npm install never warns on web-only consumers.\n */\ntype SafeAreaProps = { edges?: ReadonlyArray<string>; style?: unknown; children?: ReactNode };\n// Local `require` declaration so we don't pull @types/node into this\n// package. Metro / Node both inject `require` at module scope; in the\n// ESM build path tsup polyfills it via createRequire(import.meta.url).\ndeclare const require: (id: string) => unknown;\nlet SafeAreaView: ComponentType<SafeAreaProps>;\ntry {\n const mod = require(\"react-native-safe-area-context\") as {\n SafeAreaView: ComponentType<SafeAreaProps>;\n };\n SafeAreaView = mod.SafeAreaView;\n} catch {\n SafeAreaView = ({ children, style }: SafeAreaProps) =>\n createElement(\n View as ComponentType<{ style?: unknown; children?: ReactNode }>,\n { style },\n children,\n );\n}\n\nconst C = {\n bg: \"#0b0f0e\",\n panel: \"#111714\",\n border: \"#1f2a25\",\n text: \"#e7f5ef\",\n dim: \"#8aa79b\",\n emerald: \"#10b981\",\n emeraldSoft: \"#34d399\",\n};\n\nexport function FeedbackModal(props: {\n client: FeedbackClient;\n visible: boolean;\n keys?: DeclaredKey[];\n namespace?: string | string[];\n /** 0.2.7 — `\"skip\"` removes the built-in ToS step; the host owns\n * acceptance via `controller.acceptTos()`. A missing token surfaces\n * as a 401-derived error in the existing error row. */\n tos?: \"modal\" | \"skip\";\n onClose: () => void;\n}) {\n const { client, visible, keys, namespace, tos = \"modal\", onClose } = props;\n const [consented, setConsented] = useState(\n tos === \"skip\" ? true : client.hasConsented,\n );\n const [busy, setBusy] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [strings, setStrings] = useState<FeedbackString[]>([]);\n // 0.2.8 — \"Show original\" toggle. OFF by default; see /react panel\n // for the rationale + bundled labels (en/fr/es/de).\n const [showSource, setShowSource] = useState<boolean>(false);\n\n const loadStrings = useCallback(async () => {\n setBusy(true);\n setError(null);\n try {\n // On-screen scoping is NORMATIVE (spec ltm 373, CONTRACT v5): show\n // ONLY rendered keys. Empty -> \"no strings on this view\"; never\n // fall back to fetching the whole project.\n const resolved = resolveKeys(keys, namespace);\n if (!resolved.length) {\n // #806 SeedSower P1 — same diagnostic as the web panel: an empty\n // registry resolve with no explicit `keys` prop almost always\n // means the host's `useTranslation` imports went to\n // `react-i18next` instead of `@verbumia/react-i18next`.\n if (!keys || keys.length === 0) {\n // eslint-disable-next-line no-console\n console.warn(\n \"[verbumia/feedback] registry empty — are your useTranslation imports coming from @verbumia/react-i18next?\",\n );\n }\n setStrings([]);\n return;\n }\n const res = await client.getStrings({ keys: resolved });\n setStrings(res.strings);\n } catch (e) {\n setError(e instanceof Error ? e.message : \"Failed to load strings\");\n } finally {\n setBusy(false);\n }\n }, [client, keys, namespace]);\n\n useEffect(() => {\n if (visible && consented) void loadStrings();\n }, [visible, consented, loadStrings]);\n\n const accept = useCallback(async () => {\n setBusy(true);\n try {\n await client.acceptTos();\n setConsented(true);\n } catch (e) {\n setError(e instanceof Error ? e.message : \"Could not accept the terms\");\n } finally {\n setBusy(false);\n }\n }, [client]);\n\n return (\n <Modal\n visible={visible}\n transparent\n animationType=\"slide\"\n onRequestClose={onClose}\n >\n {/*\n #806 SeedSower native panel layout — KeyboardAvoidingView wraps the\n whole overlay so the suggestion TextInput isn't covered by the soft\n keyboard. `padding` on iOS / `height` on Android matches the RN\n community convention; flex:1 keeps the overlay full-screen above\n the keyboard reservation.\n */}\n <KeyboardAvoidingView\n style={{ flex: 1 }}\n behavior={Platform.OS === \"ios\" ? \"padding\" : \"height\"}\n >\n <View style={{ flex: 1, backgroundColor: \"rgba(0,0,0,.55)\", justifyContent: \"flex-end\" }}>\n {/*\n Panel sizing — `height:'85%'` gives a CONCRETE 85% of the\n modal's parent (the full-screen overlay), which is what the\n ToS step / strings ScrollView need to flex against. The prior\n `maxHeight:'85%'` was a ceiling only: without a height/flex\n child, the panel collapsed to its ToS intrinsic content (~15%\n of screen) on RN 0.77.3 Android. `maxHeight` is kept as a\n backstop in case a future child wants to render shorter (it\n never exceeds 85%; it can render shorter if the content does).\n\n SafeAreaView edges={['bottom']} keeps the CTA off the Android\n gesture bar / iOS home indicator. Falls back to plain View\n when react-native-safe-area-context isn't installed.\n */}\n <SafeAreaView\n edges={[\"bottom\"]}\n style={{\n height: \"85%\",\n maxHeight: \"85%\",\n backgroundColor: C.bg,\n borderTopWidth: 1,\n borderColor: C.border,\n }}\n >\n <View style={{ flex: 1, padding: 18 }}>\n <View\n style={{\n flexDirection: \"row\",\n justifyContent: \"space-between\",\n marginBottom: 12,\n }}\n >\n <Text style={{ color: C.emeraldSoft, fontWeight: \"700\", fontSize: 16 }}>\n Translation feedback\n </Text>\n <TouchableOpacity onPress={onClose} accessibilityLabel=\"Close\">\n <Text style={{ color: C.dim, fontSize: 20 }}>×</Text>\n </TouchableOpacity>\n </View>\n\n {error ? (\n <Text style={{ color: \"#f87171\", marginBottom: 8 }}>{error}</Text>\n ) : null}\n\n {!consented ? (\n <View>\n <Text style={{ color: C.text, lineHeight: 21 }}>\n Help improve the translations in this app. Your ratings and\n suggestions are sent to the app owner via Verbumia. Don’t\n submit personal or sensitive information.\n </Text>\n <TouchableOpacity\n disabled={busy}\n onPress={accept}\n style={{\n marginTop: 14,\n backgroundColor: C.emerald,\n borderRadius: 8,\n padding: 14,\n }}\n >\n <Text style={{ color: \"#03110c\", fontWeight: \"700\", textAlign: \"center\" }}>\n {busy ? \"…\" : `I accept the terms (v${client.tosVersion})`}\n </Text>\n </TouchableOpacity>\n </View>\n ) : (\n // flex:1 makes the ScrollView fill the remaining panel\n // height after the header; without it the list collapsed\n // to its intrinsic height and the panel left a large empty\n // area below the last row (the second half of the\n // SeedSower repro).\n <ScrollView style={{ flex: 1 }}>\n {!strings.length ? (\n <Text style={{ color: C.dim }}>\n {busy ? \"Loading…\" : \"No strings to review on this view.\"}\n </Text>\n ) : (\n <>\n {/* 0.2.8 — source-language toggle. Bundled labels\n en/fr/es/de (core/locales.ts). */}\n <TouchableOpacity\n onPress={() => setShowSource((v) => !v)}\n accessibilityLabel={tr(client.language, \"showOriginal\")}\n style={{\n alignSelf: \"flex-end\",\n flexDirection: \"row\",\n alignItems: \"center\",\n marginBottom: 10,\n paddingVertical: 4,\n paddingHorizontal: 8,\n borderWidth: 1,\n borderColor: C.border,\n borderRadius: 6,\n }}\n >\n <Text\n style={{\n color: showSource ? C.emeraldSoft : C.dim,\n fontSize: 12,\n marginRight: 6,\n }}\n >\n {showSource ? \"☑\" : \"☐\"}\n </Text>\n <Text style={{ color: C.dim, fontSize: 12 }}>\n {tr(client.language, \"showOriginal\")}\n </Text>\n </TouchableOpacity>\n {strings.map((s) => (\n <StringRow\n key={`${s.namespace}:${s.key}`}\n s={s}\n client={client}\n showSource={showSource}\n />\n ))}\n </>\n )}\n </ScrollView>\n )}\n </View>\n </SafeAreaView>\n </View>\n </KeyboardAvoidingView>\n </Modal>\n );\n}\n\nfunction StringRow(props: {\n s: FeedbackString;\n client: FeedbackClient;\n /** 0.2.8 — see /react panel for semantics. */\n showSource: boolean;\n}) {\n const { s, client, showSource } = props;\n const [mine, setMine] = useState<number | null>(s.my_rating);\n const [show, setShow] = useState(false);\n // 0.2.8 — pre-fill from server-persisted my_suggestion when present.\n const mySuggestionId: string | null = s.my_suggestion\n ? s.my_suggestion.id\n : null;\n const [text, setText] = useState<string>(s.my_suggestion?.text ?? \"\");\n const [sent, setSent] = useState(false);\n const [editError, setEditError] = useState<string | null>(null);\n\n const submit = async (): Promise<void> => {\n const trimmed = text.trim();\n if (!trimmed) return;\n setEditError(null);\n try {\n if (mySuggestionId) {\n // 0.2.8 edit path — synchronous PATCH so the user sees the\n // round-trip land (vs batched queue for fresh creates).\n await client.editSuggestion(mySuggestionId, trimmed);\n } else {\n client.suggest({\n namespace: s.namespace,\n key: s.key,\n language: client.language,\n translation_hash: s.translation_hash,\n suggested_text: trimmed,\n });\n }\n setSent(true);\n setShow(false);\n } catch (e) {\n setEditError(\n e instanceof Error ? e.message : \"Could not update the suggestion\",\n );\n }\n };\n\n const showSourceRow = showSource && s.source_locale !== client.language;\n const submitLabel = tr(\n client.language,\n mySuggestionId ? \"updateMySuggestion\" : \"submitSuggestion\",\n );\n\n return (\n <View\n style={{\n backgroundColor: C.panel,\n borderWidth: 1,\n borderColor: C.border,\n borderRadius: 10,\n padding: 12,\n marginBottom: 10,\n }}\n >\n <Text style={{ color: C.dim, fontSize: 12 }}>\n {s.namespace} · {s.key}\n </Text>\n {showSourceRow ? (\n <Text\n accessibilityLabel=\"source\"\n style={{\n color: C.dim,\n fontSize: 12,\n fontStyle: \"italic\",\n marginTop: 4,\n }}\n >\n {s.source_text}\n </Text>\n ) : null}\n <Text style={{ color: C.text, fontSize: 15, marginVertical: 6 }}>\n {s.value}\n </Text>\n <View style={{ flexDirection: \"row\" }}>\n {[1, 2, 3, 4, 5].map((n) => (\n <TouchableOpacity\n key={n}\n accessibilityLabel={`${n} stars`}\n onPress={() => {\n setMine(n);\n client.rate({\n namespace: s.namespace,\n key: s.key,\n language: client.language,\n translation_hash: s.translation_hash,\n stars: n,\n });\n }}\n >\n <Text\n style={{\n fontSize: 22,\n marginRight: 4,\n color: mine && n <= mine ? C.emeraldSoft : C.border,\n }}\n >\n ★\n </Text>\n </TouchableOpacity>\n ))}\n <TouchableOpacity\n onPress={() => setShow(!show)}\n style={{ marginLeft: \"auto\" }}\n >\n <Text style={{ color: C.emeraldSoft }}>\n {sent ? \"Suggested ✓\" : submitLabel}\n </Text>\n </TouchableOpacity>\n </View>\n {show ? (\n <View style={{ marginTop: 10 }}>\n <TextInput\n value={text}\n onChangeText={setText}\n multiline\n numberOfLines={3}\n placeholder=\"Your suggested translation…\"\n placeholderTextColor={C.dim}\n style={{\n backgroundColor: C.bg,\n color: C.text,\n borderWidth: 1,\n borderColor: C.border,\n borderRadius: 6,\n padding: 8,\n }}\n />\n {editError ? (\n <Text style={{ color: \"#f87171\", fontSize: 12, marginTop: 6 }}>\n {editError}\n </Text>\n ) : null}\n <TouchableOpacity\n onPress={() => void submit()}\n style={{\n marginTop: 6,\n backgroundColor: C.emerald,\n borderRadius: 6,\n padding: 10,\n }}\n >\n <Text style={{ color: \"#03110c\", fontWeight: \"700\", textAlign: \"center\" }}>\n {submitLabel}\n </Text>\n </TouchableOpacity>\n </View>\n ) : null}\n </View>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAQA,SAAS,4BAA4C;;;ACRrD,SAAS,eAAe,aAAa,WAAW,gBAAoD;AACpG;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAgKO,SAqDM,UA9CJ,KAPF;AA1Id,IAAI;AACJ,IAAI;AACF,QAAM,MAAM,UAAQ,gCAAgC;AAGpD,iBAAe,IAAI;AACrB,QAAQ;AACN,iBAAe,CAAC,EAAE,UAAU,MAAM,MAChC;AAAA,IACE;AAAA,IACA,EAAE,MAAM;AAAA,IACR;AAAA,EACF;AACJ;AAEA,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,OAU3B;AACD,QAAM,EAAE,QAAQ,SAAS,MAAM,WAAW,MAAM,SAAS,QAAQ,IAAI;AACrE,QAAM,CAAC,WAAW,YAAY,IAAI;AAAA,IAChC,QAAQ,SAAS,OAAO,OAAO;AAAA,EACjC;AACA,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAwB,IAAI;AACtD,QAAM,CAAC,SAAS,UAAU,IAAI,SAA2B,CAAC,CAAC;AAG3D,QAAM,CAAC,YAAY,aAAa,IAAI,SAAkB,KAAK;AAE3D,QAAM,cAAc,YAAY,YAAY;AAC1C,YAAQ,IAAI;AACZ,aAAS,IAAI;AACb,QAAI;AAIF,YAAM,WAAW,YAAY,MAAM,SAAS;AAC5C,UAAI,CAAC,SAAS,QAAQ;AAKpB,YAAI,CAAC,QAAQ,KAAK,WAAW,GAAG;AAE9B,kBAAQ;AAAA,YACN;AAAA,UACF;AAAA,QACF;AACA,mBAAW,CAAC,CAAC;AACb;AAAA,MACF;AACA,YAAM,MAAM,MAAM,OAAO,WAAW,EAAE,MAAM,SAAS,CAAC;AACtD,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,MAAM,SAAS,CAAC;AAE5B,YAAU,MAAM;AACd,QAAI,WAAW,UAAW,MAAK,YAAY;AAAA,EAC7C,GAAG,CAAC,SAAS,WAAW,WAAW,CAAC;AAEpC,QAAM,SAAS,YAAY,YAAY;AACrC,YAAQ,IAAI;AACZ,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;AAAA,MACA,aAAW;AAAA,MACX,eAAc;AAAA,MACd,gBAAgB;AAAA,MAShB;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,EAAE,MAAM,EAAE;AAAA,UACjB,UAAU,SAAS,OAAO,QAAQ,YAAY;AAAA,UAE9C,8BAAC,QAAK,OAAO,EAAE,MAAM,GAAG,iBAAiB,mBAAmB,gBAAgB,WAAW,GAerF;AAAA,YAAC;AAAA;AAAA,cACC,OAAO,CAAC,QAAQ;AAAA,cAChB,OAAO;AAAA,gBACL,QAAQ;AAAA,gBACR,WAAW;AAAA,gBACX,iBAAiB,EAAE;AAAA,gBACnB,gBAAgB;AAAA,gBAChB,aAAa,EAAE;AAAA,cACjB;AAAA,cAEA,+BAAC,QAAK,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,GAClC;AAAA;AAAA,kBAAC;AAAA;AAAA,oBACC,OAAO;AAAA,sBACL,eAAe;AAAA,sBACf,gBAAgB;AAAA,sBAChB,cAAc;AAAA,oBAChB;AAAA,oBAEA;AAAA,0CAAC,QAAK,OAAO,EAAE,OAAO,EAAE,aAAa,YAAY,OAAO,UAAU,GAAG,GAAG,kCAExE;AAAA,sBACA,oBAAC,oBAAiB,SAAS,SAAS,oBAAmB,SACrD,8BAAC,QAAK,OAAO,EAAE,OAAO,EAAE,KAAK,UAAU,GAAG,GAAG,kBAAC,GAChD;AAAA;AAAA;AAAA,gBACF;AAAA,gBAEC,QACC,oBAAC,QAAK,OAAO,EAAE,OAAO,WAAW,cAAc,EAAE,GAAI,iBAAM,IACzD;AAAA,gBAEH,CAAC,YACA,qBAAC,QACC;AAAA,sCAAC,QAAK,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,GAAG,GAAG,kLAIhD;AAAA,kBACA;AAAA,oBAAC;AAAA;AAAA,sBACC,UAAU;AAAA,sBACV,SAAS;AAAA,sBACT,OAAO;AAAA,wBACL,WAAW;AAAA,wBACX,iBAAiB,EAAE;AAAA,wBACnB,cAAc;AAAA,wBACd,SAAS;AAAA,sBACX;AAAA,sBAEA,8BAAC,QAAK,OAAO,EAAE,OAAO,WAAW,YAAY,OAAO,WAAW,SAAS,GACrE,iBAAO,WAAM,wBAAwB,OAAO,UAAU,KACzD;AAAA;AAAA,kBACF;AAAA,mBACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAOA,oBAAC,cAAW,OAAO,EAAE,MAAM,EAAE,GAC1B,WAAC,QAAQ,SACR,oBAAC,QAAK,OAAO,EAAE,OAAO,EAAE,IAAI,GACzB,iBAAO,kBAAa,sCACvB,IAEA,iCAGE;AAAA;AAAA,sBAAC;AAAA;AAAA,wBACC,SAAS,MAAM,cAAc,CAAC,MAAM,CAAC,CAAC;AAAA,wBACtC,oBAAoB,EAAG,OAAO,UAAU,cAAc;AAAA,wBACtD,OAAO;AAAA,0BACL,WAAW;AAAA,0BACX,eAAe;AAAA,0BACf,YAAY;AAAA,0BACZ,cAAc;AAAA,0BACd,iBAAiB;AAAA,0BACjB,mBAAmB;AAAA,0BACnB,aAAa;AAAA,0BACb,aAAa,EAAE;AAAA,0BACf,cAAc;AAAA,wBAChB;AAAA,wBAEA;AAAA;AAAA,4BAAC;AAAA;AAAA,8BACC,OAAO;AAAA,gCACL,OAAO,aAAa,EAAE,cAAc,EAAE;AAAA,gCACtC,UAAU;AAAA,gCACV,aAAa;AAAA,8BACf;AAAA,8BAEC,uBAAa,WAAM;AAAA;AAAA,0BACtB;AAAA,0BACA,oBAAC,QAAK,OAAO,EAAE,OAAO,EAAE,KAAK,UAAU,GAAG,GACvC,YAAG,OAAO,UAAU,cAAc,GACrC;AAAA;AAAA;AAAA,oBACF;AAAA,oBACC,QAAQ,IAAI,CAAC,MACZ;AAAA,sBAAC;AAAA;AAAA,wBAEC;AAAA,wBACA;AAAA,wBACA;AAAA;AAAA,sBAHK,GAAG,EAAE,SAAS,IAAI,EAAE,GAAG;AAAA,oBAI9B,CACD;AAAA,qBACH,GAEJ;AAAA;AAAA,iBAEJ;AAAA;AAAA,UACF,GACF;AAAA;AAAA,MACF;AAAA;AAAA,EACF;AAEJ;AAEA,SAAS,UAAU,OAKhB;AACD,QAAM,EAAE,GAAG,QAAQ,WAAW,IAAI;AAClC,QAAM,CAAC,MAAM,OAAO,IAAI,SAAwB,EAAE,SAAS;AAC3D,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AAEtC,QAAM,iBAAgC,EAAE,gBACpC,EAAE,cAAc,KAChB;AACJ,QAAM,CAAC,MAAM,OAAO,IAAI,SAAiB,EAAE,eAAe,QAAQ,EAAE;AACpE,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,CAAC,WAAW,YAAY,IAAI,SAAwB,IAAI;AAE9D,QAAM,SAAS,YAA2B;AACxC,UAAM,UAAU,KAAK,KAAK;AAC1B,QAAI,CAAC,QAAS;AACd,iBAAa,IAAI;AACjB,QAAI;AACF,UAAI,gBAAgB;AAGlB,cAAM,OAAO,eAAe,gBAAgB,OAAO;AAAA,MACrD,OAAO;AACL,eAAO,QAAQ;AAAA,UACb,WAAW,EAAE;AAAA,UACb,KAAK,EAAE;AAAA,UACP,UAAU,OAAO;AAAA,UACjB,kBAAkB,EAAE;AAAA,UACpB,gBAAgB;AAAA,QAClB,CAAC;AAAA,MACH;AACA,cAAQ,IAAI;AACZ,cAAQ,KAAK;AAAA,IACf,SAAS,GAAG;AACV;AAAA,QACE,aAAa,QAAQ,EAAE,UAAU;AAAA,MACnC;AAAA,IACF;AAAA,EACF;AAEA,QAAM,gBAAgB,cAAc,EAAE,kBAAkB,OAAO;AAC/D,QAAM,cAAc;AAAA,IAClB,OAAO;AAAA,IACP,iBAAiB,uBAAuB;AAAA,EAC1C;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAO;AAAA,QACL,iBAAiB,EAAE;AAAA,QACnB,aAAa;AAAA,QACb,aAAa,EAAE;AAAA,QACf,cAAc;AAAA,QACd,SAAS;AAAA,QACT,cAAc;AAAA,MAChB;AAAA,MAEA;AAAA,6BAAC,QAAK,OAAO,EAAE,OAAO,EAAE,KAAK,UAAU,GAAG,GACvC;AAAA,YAAE;AAAA,UAAU;AAAA,UAAI,EAAE;AAAA,WACrB;AAAA,QACC,gBACC;AAAA,UAAC;AAAA;AAAA,YACC,oBAAmB;AAAA,YACnB,OAAO;AAAA,cACL,OAAO,EAAE;AAAA,cACT,UAAU;AAAA,cACV,WAAW;AAAA,cACX,WAAW;AAAA,YACb;AAAA,YAEC,YAAE;AAAA;AAAA,QACL,IACE;AAAA,QACJ,oBAAC,QAAK,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,IAAI,gBAAgB,EAAE,GAC3D,YAAE,OACL;AAAA,QACA,qBAAC,QAAK,OAAO,EAAE,eAAe,MAAM,GACjC;AAAA,WAAC,GAAG,GAAG,GAAG,GAAG,CAAC,EAAE,IAAI,CAAC,MACpB;AAAA,YAAC;AAAA;AAAA,cAEC,oBAAoB,GAAG,CAAC;AAAA,cACxB,SAAS,MAAM;AACb,wBAAQ,CAAC;AACT,uBAAO,KAAK;AAAA,kBACV,WAAW,EAAE;AAAA,kBACb,KAAK,EAAE;AAAA,kBACP,UAAU,OAAO;AAAA,kBACjB,kBAAkB,EAAE;AAAA,kBACpB,OAAO;AAAA,gBACT,CAAC;AAAA,cACH;AAAA,cAEA;AAAA,gBAAC;AAAA;AAAA,kBACC,OAAO;AAAA,oBACL,UAAU;AAAA,oBACV,aAAa;AAAA,oBACb,OAAO,QAAQ,KAAK,OAAO,EAAE,cAAc,EAAE;AAAA,kBAC/C;AAAA,kBACD;AAAA;AAAA,cAED;AAAA;AAAA,YArBK;AAAA,UAsBP,CACD;AAAA,UACD;AAAA,YAAC;AAAA;AAAA,cACC,SAAS,MAAM,QAAQ,CAAC,IAAI;AAAA,cAC5B,OAAO,EAAE,YAAY,OAAO;AAAA,cAE5B,8BAAC,QAAK,OAAO,EAAE,OAAO,EAAE,YAAY,GACjC,iBAAO,qBAAgB,aAC1B;AAAA;AAAA,UACF;AAAA,WACF;AAAA,QACC,OACC,qBAAC,QAAK,OAAO,EAAE,WAAW,GAAG,GAC3B;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP,cAAc;AAAA,cACd,WAAS;AAAA,cACT,eAAe;AAAA,cACf,aAAY;AAAA,cACZ,sBAAsB,EAAE;AAAA,cACxB,OAAO;AAAA,gBACL,iBAAiB,EAAE;AAAA,gBACnB,OAAO,EAAE;AAAA,gBACT,aAAa;AAAA,gBACb,aAAa,EAAE;AAAA,gBACf,cAAc;AAAA,gBACd,SAAS;AAAA,cACX;AAAA;AAAA,UACF;AAAA,UACC,YACC,oBAAC,QAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,WAAW,EAAE,GACzD,qBACH,IACE;AAAA,UACJ;AAAA,YAAC;AAAA;AAAA,cACC,SAAS,MAAM,KAAK,OAAO;AAAA,cAC3B,OAAO;AAAA,gBACL,WAAW;AAAA,gBACX,iBAAiB,EAAE;AAAA,gBACnB,cAAc;AAAA,gBACd,SAAS;AAAA,cACX;AAAA,cAEA,8BAAC,QAAK,OAAO,EAAE,OAAO,WAAW,YAAY,OAAO,WAAW,SAAS,GACrE,uBACH;AAAA;AAAA,UACF;AAAA,WACF,IACE;AAAA;AAAA;AAAA,EACN;AAEJ;;;ADrPM,gBAAAA,YAAA;AAnFN,SAAS,YAAY;AACnB,MAAI,QAAoB,EAAE,QAAQ,OAAO,cAAc,OAAU;AACjE,QAAM,YAAY,oBAAI,IAAgB;AACtC,SAAO;AAAA,IACL,UAAU,MAAkB;AAAA,IAC5B,QAAQ,MAAe,cAAoC;AACzD,YAAM,OAAmB;AAAA,QACvB,QAAQ;AAAA,QACR,cAAc,OAAO,eAAe;AAAA,MACtC;AACA,UACE,MAAM,WAAW,KAAK,UACtB,MAAM,iBAAiB,KAAK,cAC5B;AACA;AAAA,MACF;AACA,cAAQ;AACR,gBAAU,QAAQ,CAAC,MAAM,EAAE,CAAC;AAAA,IAC9B;AAAA,IACA,UAAU,GAA2B;AACnC,gBAAU,IAAI,CAAC;AACf,aAAO,MAAM;AACX,kBAAU,OAAO,CAAC;AAAA,MACpB;AAAA,IACF;AAAA,EACF;AACF;AAMA,eAAe,2BACb,SAMwB;AAKxB,QAAM,MAAO,WACV;AACH,MAAI,CAAC,IAAK,QAAO,CAAC;AAClB,QAAM,MAAM,SAAS;AACrB,QAAM,SAAS,SAAS;AACxB,MAAI,OAAO,WAAW,cAAc,OAAO,QAAQ,YAAY,KAAK;AAClE,QAAI,QAAQ;AACZ,QAAI;AACF,YAAM,OAAO,GAAG;AAAA,IAClB,QAAQ;AAAA,IAER;AACA,UAAM,IAAI,QAAc,CAAC,MAAM;AAC7B,UAAI,OAAO,0BAA0B,YAAY;AAC/C,8BAAsB,MAAM,EAAE,CAAC;AAAA,MACjC,OAAO;AACL,mBAAW,MAAM,EAAE,GAAG,EAAE;AAAA,MAC1B;AAAA,IACF,CAAC;AACD,WAAO,IAAI,SAAS;AAAA,EACtB;AACA,SAAO,IAAI,SAAS;AACtB;AAEO,SAAS,eAAe,SAA4C;AACzE,QAAM,QAAQ,UAAU;AACxB,MAAI,SAAgC;AAEpC,WAAS,SAAS;AAChB,UAAM,QAAQ;AAAA,MACZ,MAAM;AAAA,MACN,MAAM;AAAA,MACN,MAAM;AAAA,IACR;AACA,QAAI,CAAC,OAAQ,QAAO;AACpB,UAAM,IAAI;AAEV,UAAM,YAAY,QAAQ,QAAQ,MAAM;AACxC,WACE,gBAAAA;AAAA,MAAC;AAAA;AAAA,QACC,QAAQ;AAAA,QACR,SAAS,MAAM;AAAA,QACf,MAAM;AAAA,QACN,WAAW,QAAQ;AAAA,QACnB,KAAK,QAAQ,OAAO;AAAA,QACpB,SAAS,MAAM;AACb,gBAAM,QAAQ,KAAK;AACnB,eAAK,EAAE,MAAM;AAAA,QACf;AAAA;AAAA,IACF;AAAA,EAEJ;AAEA,SAAO;AAAA,IACL,MAAM;AAAA,IACN,MAAM,KAAK;AAIT,YAAM,kBACJ,QAAQ,YAAY,IAAI,MAAM,YAAY,IAAI,OAAO;AAIvD,YAAM,MAAwB,QAAQ,OAAO;AAC7C,eAAS,IAAI,eAAe;AAAA,QAC1B,SACE,QAAQ,WAAW,IAAI,OAAO,WAAW;AAAA,QAC3C,WAAW,QAAQ,aAAa,IAAI,OAAO;AAAA,QAC3C,UAAU;AAAA,QACV,WAAW,QAAQ;AAAA,QACnB,WAAW,QAAQ;AAAA,QACnB,QAAQ,QAAQ;AAAA,QAChB,eAAe,QAAQ;AAAA,MACzB,CAAC;AACD,YAAM,YAAY;AAElB,UAAI,aAAwC;AAC5C,YAAM,MAAgC,QAAQ,OAAO;AAErD,YAAM,QAAgC,QAAQ,SAAS;AACvD,YAAM,aAAa,IAAI,MAAM;AAC7B,YAAM,eAAe,YAA2B;AAC9C,YAAI;AACF,gBAAM,OAAO,MAAM,UAAU,cAAc;AAC3C,cAAI,SAAS,KAAM,cAAa;AAAA,QAClC,QAAQ;AAAA,QAER;AAAA,MACF;AACA,UAAI,QAAQ,OAAQ,MAAK,aAAa;AAEtC,UAAI;AACJ,UAAI,OAAO,IAAI,qBAAqB,YAAY;AAC9C,oBAAY,IAAI,iBAAiB,CAAC,QAAQ;AACxC,oBAAU,YAAY,GAAG;AACzB,eAAK,aAAa;AAAA,QACpB,CAAC;AAAA,MACH;AACA,YAAM,aAAiC;AAAA,QACrC,MAAM,YAA2B;AAC/B,cAAI,UAAU,gBAAgB;AAC5B,kBAAM,WAAW,MAAM,2BAA2B,UAAU;AAC5D,kBAAM,QAAQ,MAAM,QAAQ;AAC5B;AAAA,UACF;AACA,gBAAM,QAAQ,IAAI;AAAA,QACpB;AAAA,QACA,OAAO,MAAM;AACX,gBAAM,QAAQ,KAAK;AACnB,eAAK,WAAW,MAAM;AAAA,QACxB;AAAA,QACA,QAAQ;AAAA,QACR,IAAI,aAAqB;AACvB,iBAAO,UAAU;AAAA,QACnB;AAAA,QACA,IAAI,iBAA0B;AAC5B,iBAAO,UAAU;AAAA,QACnB;AAAA,QACA,WAAW,YAA2B;AACpC,gBAAM,UAAU,UAAU;AAC1B,cAAI,CAAC,QAAQ,OAAQ,OAAM,aAAa;AAAA,QAC1C;AAAA,QACA,IAAI,WAA2B;AAC7B,cAAI,QAAQ,OAAQ,QAAO;AAC3B,cAAI,QAAQ,OAAQ,QAAO;AAC3B,iBAAO,aAAa,WAAW,WAAW;AAAA,QAC5C;AAAA,QACA,IAAI,mBAA4C;AAC9C,iBAAO,aAAa,WAAW,mBAAmB;AAAA,QACpD;AAAA,QACA,IAAI,MAAwD;AAC1D,iBAAO,aAAa,WAAW,MAAM;AAAA,QACvC;AAAA,MACF;AACA,cAAQ,UAAU,UAAU;AAC5B,UAAI,QAAQ,cAAe,SAAQ,cAAc,UAAU;AAC3D,aAAO,MAAM;AACX,oBAAY;AACZ,YAAI,QAAQ,cAAe,SAAQ,cAAc,UAAU;AAC3D,aAAK,QAAQ,MAAM;AAAA,MACrB;AAAA,IACF;AAAA,IACA,QAAQ,MAAM,gBAAAA,KAAC,UAAO;AAAA,EACxB;AACF;","names":["jsx"]}
|