@indesad/feedback-widget 0.0.0-stage → 1.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/README.md +93 -3
- package/dist/i18n.d.ts +44 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1012 -0
- package/dist/sha256.d.ts +7 -0
- package/dist/styles.d.ts +1 -0
- package/dist/types.d.ts +97 -0
- package/dist/widget.d.ts +67 -0
- package/package.json +31 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,1012 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
|
|
5
|
+
// packages/web/feedback-widget/src/i18n.ts
|
|
6
|
+
var COPY = {
|
|
7
|
+
"es-419": {
|
|
8
|
+
launcher: "Opiniones",
|
|
9
|
+
title: "\xBFC\xF3mo podemos mejorar?",
|
|
10
|
+
subtitle: "Tu opini\xF3n llega directo al equipo de {app}.",
|
|
11
|
+
close: "Cerrar",
|
|
12
|
+
back: "Volver",
|
|
13
|
+
kinds: {
|
|
14
|
+
bug: { label: "Reportar un problema", hint: "Algo no funciona como esperabas", placeholder: "\xBFQu\xE9 estabas haciendo y qu\xE9 pas\xF3? Mientras m\xE1s detalle, m\xE1s r\xE1pido lo resolvemos." },
|
|
15
|
+
improvement: { label: "Sugerir una mejora", hint: "Una idea para hacerlo mejor", placeholder: "Cu\xE9ntanos tu idea y para qu\xE9 te servir\xEDa." },
|
|
16
|
+
question: { label: "Tengo una pregunta", hint: "Algo no est\xE1 claro", placeholder: "\xBFQu\xE9 te gustar\xEDa saber?" },
|
|
17
|
+
praise: { label: "Me gust\xF3 algo", hint: "Cu\xE9ntanos qu\xE9 te encanta", placeholder: "\xBFQu\xE9 es lo que m\xE1s te gusta?" }
|
|
18
|
+
},
|
|
19
|
+
needHelp: "\xBFNecesitas ayuda ahora? Habla con soporte",
|
|
20
|
+
message: "Tu mensaje",
|
|
21
|
+
name: "Tu nombre",
|
|
22
|
+
email: "Tu correo",
|
|
23
|
+
emailOptional: "Opcional: para responderte",
|
|
24
|
+
sendingAs: "Se enviar\xE1 como {who}",
|
|
25
|
+
attach: "Adjuntar capturas",
|
|
26
|
+
attachMore: "Agregar otra captura",
|
|
27
|
+
attachHint: "PNG, JPG o WebP, hasta {max} im\xE1genes de {mb} MB. Tambi\xE9n puedes pegarlas (Ctrl+V) en el mensaje",
|
|
28
|
+
remove: "Quitar",
|
|
29
|
+
send: "Enviar",
|
|
30
|
+
sending: "Enviando\u2026",
|
|
31
|
+
privacy: "Junto con tu mensaje enviamos la pantalla en que est\xE1s y la versi\xF3n de la app, para entender el contexto.",
|
|
32
|
+
successTitle: "\xA1Gracias por escribirnos!",
|
|
33
|
+
successBody: "Recibimos tu opini\xF3n con el folio {folio}. Tu opini\xF3n es importante: nuestro equipo la va a revisar.",
|
|
34
|
+
successEmail: "Te enviamos una copia a {email} y te avisaremos por ah\xED.",
|
|
35
|
+
viewStatus: "Ver el estado",
|
|
36
|
+
another: "Enviar otra",
|
|
37
|
+
errorGeneric: "No pudimos enviar tu opini\xF3n. Revisa tu conexi\xF3n y vuelve a intentarlo.",
|
|
38
|
+
errorMessage: "Escribe al menos 3 caracteres.",
|
|
39
|
+
errorName: "Escribe tu nombre.",
|
|
40
|
+
errorEmail: "Escribe un correo v\xE1lido.",
|
|
41
|
+
errorAttachmentType: "La captura debe ser PNG, JPG o WebP.",
|
|
42
|
+
errorAttachmentSize: "La captura supera los {mb} MB.",
|
|
43
|
+
errorAttachmentCount: "Puedes adjuntar hasta {max} im\xE1genes.",
|
|
44
|
+
poweredBy: "Con tecnolog\xEDa de IndeFlow",
|
|
45
|
+
counter: "{n} de {max}"
|
|
46
|
+
},
|
|
47
|
+
en: {
|
|
48
|
+
launcher: "Feedback",
|
|
49
|
+
title: "How can we improve?",
|
|
50
|
+
subtitle: "Your feedback goes straight to the {app} team.",
|
|
51
|
+
close: "Close",
|
|
52
|
+
back: "Back",
|
|
53
|
+
kinds: {
|
|
54
|
+
bug: { label: "Report a problem", hint: "Something is not working as expected", placeholder: "What were you doing and what happened? The more detail, the faster we can fix it." },
|
|
55
|
+
improvement: { label: "Suggest an improvement", hint: "An idea to make it better", placeholder: "Tell us your idea and how it would help you." },
|
|
56
|
+
question: { label: "Ask a question", hint: "Something is not clear", placeholder: "What would you like to know?" },
|
|
57
|
+
praise: { label: "I liked something", hint: "Tell us what you love", placeholder: "What do you like the most?" }
|
|
58
|
+
},
|
|
59
|
+
needHelp: "Need help right now? Talk to support",
|
|
60
|
+
message: "Your message",
|
|
61
|
+
name: "Your name",
|
|
62
|
+
email: "Your email",
|
|
63
|
+
emailOptional: "Optional: so we can reply",
|
|
64
|
+
sendingAs: "Will be sent as {who}",
|
|
65
|
+
attach: "Attach screenshots",
|
|
66
|
+
attachMore: "Add another screenshot",
|
|
67
|
+
attachHint: "PNG, JPG or WebP, up to {max} images of {mb} MB. You can also paste them (Ctrl+V) into the message",
|
|
68
|
+
remove: "Remove",
|
|
69
|
+
send: "Send",
|
|
70
|
+
sending: "Sending\u2026",
|
|
71
|
+
privacy: "Along with your message we send the screen you are on and the app version, to understand the context.",
|
|
72
|
+
successTitle: "Thanks for writing to us!",
|
|
73
|
+
successBody: "We received your feedback with reference {folio}. Your opinion matters: our team will review it.",
|
|
74
|
+
successEmail: "We sent a copy to {email} and will update you there.",
|
|
75
|
+
viewStatus: "View status",
|
|
76
|
+
another: "Send another",
|
|
77
|
+
errorGeneric: "We could not send your feedback. Check your connection and try again.",
|
|
78
|
+
errorMessage: "Write at least 3 characters.",
|
|
79
|
+
errorName: "Enter your name.",
|
|
80
|
+
errorEmail: "Enter a valid email.",
|
|
81
|
+
errorAttachmentType: "The screenshot must be PNG, JPG or WebP.",
|
|
82
|
+
errorAttachmentSize: "The screenshot is larger than {mb} MB.",
|
|
83
|
+
errorAttachmentCount: "You can attach up to {max} images.",
|
|
84
|
+
poweredBy: "Powered by IndeFlow",
|
|
85
|
+
counter: "{n} of {max}"
|
|
86
|
+
},
|
|
87
|
+
"pt-BR": {
|
|
88
|
+
launcher: "Opini\xF5es",
|
|
89
|
+
title: "Como podemos melhorar?",
|
|
90
|
+
subtitle: "Sua opini\xE3o vai direto para a equipe do {app}.",
|
|
91
|
+
close: "Fechar",
|
|
92
|
+
back: "Voltar",
|
|
93
|
+
kinds: {
|
|
94
|
+
bug: { label: "Reportar um problema", hint: "Algo n\xE3o funciona como esperado", placeholder: "O que voc\xEA estava fazendo e o que aconteceu? Quanto mais detalhes, mais r\xE1pido resolvemos." },
|
|
95
|
+
improvement: { label: "Sugerir uma melhoria", hint: "Uma ideia para deixar melhor", placeholder: "Conte sua ideia e como ela ajudaria voc\xEA." },
|
|
96
|
+
question: { label: "Tenho uma pergunta", hint: "Algo n\xE3o est\xE1 claro", placeholder: "O que voc\xEA gostaria de saber?" },
|
|
97
|
+
praise: { label: "Gostei de algo", hint: "Conte o que voc\xEA adora", placeholder: "Do que voc\xEA mais gosta?" }
|
|
98
|
+
},
|
|
99
|
+
needHelp: "Precisa de ajuda agora? Fale com o suporte",
|
|
100
|
+
message: "Sua mensagem",
|
|
101
|
+
name: "Seu nome",
|
|
102
|
+
email: "Seu e-mail",
|
|
103
|
+
emailOptional: "Opcional: para responder voc\xEA",
|
|
104
|
+
sendingAs: "Ser\xE1 enviado como {who}",
|
|
105
|
+
attach: "Anexar capturas",
|
|
106
|
+
attachMore: "Adicionar outra captura",
|
|
107
|
+
attachHint: "PNG, JPG ou WebP, at\xE9 {max} imagens de {mb} MB. Tamb\xE9m pode col\xE1-las (Ctrl+V) na mensagem",
|
|
108
|
+
remove: "Remover",
|
|
109
|
+
send: "Enviar",
|
|
110
|
+
sending: "Enviando\u2026",
|
|
111
|
+
privacy: "Junto com sua mensagem enviamos a tela em que voc\xEA est\xE1 e a vers\xE3o do aplicativo, para entender o contexto.",
|
|
112
|
+
successTitle: "Obrigado por escrever!",
|
|
113
|
+
successBody: "Recebemos sua opini\xE3o com o protocolo {folio}. Sua opini\xE3o \xE9 importante: nossa equipe vai analis\xE1-la.",
|
|
114
|
+
successEmail: "Enviamos uma c\xF3pia para {email} e daremos retorno por l\xE1.",
|
|
115
|
+
viewStatus: "Ver o status",
|
|
116
|
+
another: "Enviar outra",
|
|
117
|
+
errorGeneric: "N\xE3o foi poss\xEDvel enviar sua opini\xE3o. Verifique sua conex\xE3o e tente novamente.",
|
|
118
|
+
errorMessage: "Escreva pelo menos 3 caracteres.",
|
|
119
|
+
errorName: "Informe seu nome.",
|
|
120
|
+
errorEmail: "Informe um e-mail v\xE1lido.",
|
|
121
|
+
errorAttachmentType: "A captura deve ser PNG, JPG ou WebP.",
|
|
122
|
+
errorAttachmentSize: "A captura passa de {mb} MB.",
|
|
123
|
+
errorAttachmentCount: "Voc\xEA pode anexar at\xE9 {max} imagens.",
|
|
124
|
+
poweredBy: "Com tecnologia IndeFlow",
|
|
125
|
+
counter: "{n} de {max}"
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
function resolveLocale(requested, fallback) {
|
|
129
|
+
const pick = (tag) => {
|
|
130
|
+
if (!tag) return null;
|
|
131
|
+
const base = tag.toLowerCase().split("-")[0];
|
|
132
|
+
return base === "es" ? "es-419" : base === "en" ? "en" : base === "pt" ? "pt-BR" : null;
|
|
133
|
+
};
|
|
134
|
+
if (requested && requested !== "auto") return pick(requested) ?? pick(fallback) ?? "es-419";
|
|
135
|
+
const nav = typeof navigator !== "undefined" ? navigator.languages?.[0] ?? navigator.language : null;
|
|
136
|
+
return pick(nav) ?? pick(fallback) ?? "es-419";
|
|
137
|
+
}
|
|
138
|
+
function format(text, values) {
|
|
139
|
+
return text.replace(/\{(\w+)\}/g, (_, key) => key in values ? String(values[key]) : `{${key}}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// packages/web/feedback-widget/src/sha256.ts
|
|
143
|
+
var K = new Uint32Array([
|
|
144
|
+
1116352408,
|
|
145
|
+
1899447441,
|
|
146
|
+
3049323471,
|
|
147
|
+
3921009573,
|
|
148
|
+
961987163,
|
|
149
|
+
1508970993,
|
|
150
|
+
2453635748,
|
|
151
|
+
2870763221,
|
|
152
|
+
3624381080,
|
|
153
|
+
310598401,
|
|
154
|
+
607225278,
|
|
155
|
+
1426881987,
|
|
156
|
+
1925078388,
|
|
157
|
+
2162078206,
|
|
158
|
+
2614888103,
|
|
159
|
+
3248222580,
|
|
160
|
+
3835390401,
|
|
161
|
+
4022224774,
|
|
162
|
+
264347078,
|
|
163
|
+
604807628,
|
|
164
|
+
770255983,
|
|
165
|
+
1249150122,
|
|
166
|
+
1555081692,
|
|
167
|
+
1996064986,
|
|
168
|
+
2554220882,
|
|
169
|
+
2821834349,
|
|
170
|
+
2952996808,
|
|
171
|
+
3210313671,
|
|
172
|
+
3336571891,
|
|
173
|
+
3584528711,
|
|
174
|
+
113926993,
|
|
175
|
+
338241895,
|
|
176
|
+
666307205,
|
|
177
|
+
773529912,
|
|
178
|
+
1294757372,
|
|
179
|
+
1396182291,
|
|
180
|
+
1695183700,
|
|
181
|
+
1986661051,
|
|
182
|
+
2177026350,
|
|
183
|
+
2456956037,
|
|
184
|
+
2730485921,
|
|
185
|
+
2820302411,
|
|
186
|
+
3259730800,
|
|
187
|
+
3345764771,
|
|
188
|
+
3516065817,
|
|
189
|
+
3600352804,
|
|
190
|
+
4094571909,
|
|
191
|
+
275423344,
|
|
192
|
+
430227734,
|
|
193
|
+
506948616,
|
|
194
|
+
659060556,
|
|
195
|
+
883997877,
|
|
196
|
+
958139571,
|
|
197
|
+
1322822218,
|
|
198
|
+
1537002063,
|
|
199
|
+
1747873779,
|
|
200
|
+
1955562222,
|
|
201
|
+
2024104815,
|
|
202
|
+
2227730452,
|
|
203
|
+
2361852424,
|
|
204
|
+
2428436474,
|
|
205
|
+
2756734187,
|
|
206
|
+
3204031479,
|
|
207
|
+
3329325298
|
|
208
|
+
]);
|
|
209
|
+
var encoder = typeof TextEncoder !== "undefined" ? new TextEncoder() : null;
|
|
210
|
+
function utf8(text) {
|
|
211
|
+
if (encoder) return encoder.encode(text);
|
|
212
|
+
const out = [];
|
|
213
|
+
for (const ch of unescape(encodeURIComponent(text))) out.push(ch.charCodeAt(0));
|
|
214
|
+
return new Uint8Array(out);
|
|
215
|
+
}
|
|
216
|
+
function sha256Hex(text) {
|
|
217
|
+
const bytes = utf8(text);
|
|
218
|
+
const bitLength = bytes.length * 8;
|
|
219
|
+
const padded = new Uint8Array(bytes.length + 9 + 63 >> 6 << 6);
|
|
220
|
+
padded.set(bytes);
|
|
221
|
+
padded[bytes.length] = 128;
|
|
222
|
+
const view = new DataView(padded.buffer);
|
|
223
|
+
view.setUint32(padded.length - 8, Math.floor(bitLength / 4294967296));
|
|
224
|
+
view.setUint32(padded.length - 4, bitLength >>> 0);
|
|
225
|
+
const h = new Uint32Array([1779033703, 3144134277, 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225]);
|
|
226
|
+
const w = new Uint32Array(64);
|
|
227
|
+
for (let offset = 0; offset < padded.length; offset += 64) {
|
|
228
|
+
for (let i = 0; i < 16; i++) w[i] = view.getUint32(offset + i * 4);
|
|
229
|
+
for (let i = 16; i < 64; i++) {
|
|
230
|
+
const a2 = w[i - 15], b2 = w[i - 2];
|
|
231
|
+
const s0 = (a2 >>> 7 | a2 << 25) ^ (a2 >>> 18 | a2 << 14) ^ a2 >>> 3;
|
|
232
|
+
const s1 = (b2 >>> 17 | b2 << 15) ^ (b2 >>> 19 | b2 << 13) ^ b2 >>> 10;
|
|
233
|
+
w[i] = w[i - 16] + s0 + w[i - 7] + s1 >>> 0;
|
|
234
|
+
}
|
|
235
|
+
let a = h[0], b = h[1], c = h[2], d = h[3], e = h[4], f = h[5], g = h[6], k = h[7];
|
|
236
|
+
for (let i = 0; i < 64; i++) {
|
|
237
|
+
const S1 = (e >>> 6 | e << 26) ^ (e >>> 11 | e << 21) ^ (e >>> 25 | e << 7);
|
|
238
|
+
const ch = e & f ^ ~e & g;
|
|
239
|
+
const t1 = k + S1 + ch + K[i] + w[i] >>> 0;
|
|
240
|
+
const S0 = (a >>> 2 | a << 30) ^ (a >>> 13 | a << 19) ^ (a >>> 22 | a << 10);
|
|
241
|
+
const maj = a & b ^ a & c ^ b & c;
|
|
242
|
+
const t2 = S0 + maj >>> 0;
|
|
243
|
+
k = g;
|
|
244
|
+
g = f;
|
|
245
|
+
f = e;
|
|
246
|
+
e = d + t1 >>> 0;
|
|
247
|
+
d = c;
|
|
248
|
+
c = b;
|
|
249
|
+
b = a;
|
|
250
|
+
a = t1 + t2 >>> 0;
|
|
251
|
+
}
|
|
252
|
+
h[0] = h[0] + a >>> 0;
|
|
253
|
+
h[1] = h[1] + b >>> 0;
|
|
254
|
+
h[2] = h[2] + c >>> 0;
|
|
255
|
+
h[3] = h[3] + d >>> 0;
|
|
256
|
+
h[4] = h[4] + e >>> 0;
|
|
257
|
+
h[5] = h[5] + f >>> 0;
|
|
258
|
+
h[6] = h[6] + g >>> 0;
|
|
259
|
+
h[7] = h[7] + k >>> 0;
|
|
260
|
+
}
|
|
261
|
+
let hex = "";
|
|
262
|
+
for (const v of h) hex += v.toString(16).padStart(8, "0");
|
|
263
|
+
return hex.toUpperCase();
|
|
264
|
+
}
|
|
265
|
+
async function solveChallenge(token, difficulty = 4, maxAttempts = 3e6) {
|
|
266
|
+
const prefix = "0".repeat(difficulty);
|
|
267
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
268
|
+
if (sha256Hex(`${token}:${i}`).startsWith(prefix)) return String(i);
|
|
269
|
+
if (i % 4e3 === 3999) await new Promise((resolve) => setTimeout(resolve, 0));
|
|
270
|
+
}
|
|
271
|
+
throw new Error("challenge-unsolved");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// packages/web/feedback-widget/src/styles.ts
|
|
275
|
+
var STYLES = `
|
|
276
|
+
:host {
|
|
277
|
+
all: initial;
|
|
278
|
+
--iff-accent: var(--if-brand, #FD0055);
|
|
279
|
+
--iff-accent-hover: var(--if-brand-hover, #E0004B);
|
|
280
|
+
--iff-accent-soft: var(--if-brand-soft, rgba(253, 0, 85, 0.12));
|
|
281
|
+
--iff-accent-contrast: #FFFFFF;
|
|
282
|
+
--iff-bg: var(--if-bg-elevated, #FFFFFF);
|
|
283
|
+
--iff-surface: var(--if-bg-surface, #F8F9FA);
|
|
284
|
+
--iff-hover: var(--if-bg-hover, #F1F3F5);
|
|
285
|
+
--iff-text: var(--if-text-primary, #000002);
|
|
286
|
+
--iff-muted: var(--if-text-tertiary, #64748B);
|
|
287
|
+
--iff-border: var(--if-border-subtle, #E2E8F0);
|
|
288
|
+
--iff-border-strong: var(--if-border-default, #CBD5E1);
|
|
289
|
+
--iff-danger: var(--if-danger, #EF4444);
|
|
290
|
+
--iff-danger-soft: var(--if-danger-soft, rgba(239, 68, 68, 0.12));
|
|
291
|
+
--iff-success: var(--if-success, #10B981);
|
|
292
|
+
--iff-success-soft: var(--if-success-soft, rgba(16, 185, 129, 0.12));
|
|
293
|
+
--iff-shadow: var(--if-shadow-overlay, 0 20px 25px -5px rgba(0, 0, 0, 0.15), 0 8px 10px -6px rgba(0, 0, 0, 0.1));
|
|
294
|
+
--iff-radius: var(--if-radius-lg, 14px);
|
|
295
|
+
--iff-radius-sm: var(--if-radius-md, 10px);
|
|
296
|
+
--iff-font: var(--if-font-sans, 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
|
|
297
|
+
--iff-font-heading: var(--if-font-heading, 'Oxanium', 'Inter', sans-serif);
|
|
298
|
+
position: fixed; z-index: var(--iff-z, 2147483000); font-family: var(--iff-font); color: var(--iff-text);
|
|
299
|
+
-webkit-font-smoothing: antialiased; line-height: 1.4; font-size: 14px;
|
|
300
|
+
}
|
|
301
|
+
:host([data-theme="dark"]) {
|
|
302
|
+
--iff-bg: var(--if-bg-elevated, #262626);
|
|
303
|
+
--iff-surface: var(--if-bg-surface, #181818);
|
|
304
|
+
--iff-hover: var(--if-bg-hover, #2A2A2A);
|
|
305
|
+
--iff-text: var(--if-text-primary, #E2E8F0);
|
|
306
|
+
--iff-muted: var(--if-text-tertiary, #94A3B8);
|
|
307
|
+
--iff-border: var(--if-border-subtle, rgba(255, 255, 255, 0.08));
|
|
308
|
+
--iff-border-strong: var(--if-border-default, rgba(255, 255, 255, 0.14));
|
|
309
|
+
--iff-shadow: var(--if-shadow-overlay, 0 20px 30px -5px rgba(0, 0, 0, 0.9));
|
|
310
|
+
}
|
|
311
|
+
* { box-sizing: border-box; }
|
|
312
|
+
button, input, textarea { font: inherit; color: inherit; }
|
|
313
|
+
[hidden] { display: none !important; }
|
|
314
|
+
.launcher {
|
|
315
|
+
display: inline-flex; align-items: center; gap: 8px; height: 44px; padding: 0 16px 0 12px; border: none; border-radius: 9999px;
|
|
316
|
+
background: var(--iff-accent); color: var(--iff-accent-contrast); font-weight: 600; font-size: 14px; cursor: pointer;
|
|
317
|
+
box-shadow: 0 8px 20px -6px rgba(253, 0, 85, 0.45), 0 2px 6px rgba(0, 0, 0, 0.12); transition: transform .15s ease, background .15s ease;
|
|
318
|
+
}
|
|
319
|
+
.launcher:hover { background: var(--iff-accent-hover); transform: translateY(-1px); }
|
|
320
|
+
.launcher.icon-only { width: 44px; padding: 0; justify-content: center; }
|
|
321
|
+
.launcher.icon-only .label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
|
322
|
+
.launcher:focus-visible, button:focus-visible, a:focus-visible, input:focus-visible, textarea:focus-visible, label.file:focus-within {
|
|
323
|
+
outline: 2px solid var(--iff-accent); outline-offset: 2px;
|
|
324
|
+
}
|
|
325
|
+
.launcher svg, .launcher img { width: 20px; height: 20px; flex: none; }
|
|
326
|
+
.launcher .emoji { font-size: 18px; line-height: 1; }
|
|
327
|
+
.panel {
|
|
328
|
+
position: fixed; width: 380px; max-width: calc(100vw - 24px); max-height: min(640px, calc(100vh - 96px)); display: flex; flex-direction: column;
|
|
329
|
+
background: var(--iff-bg); color: var(--iff-text); border: 1px solid var(--iff-border); border-radius: var(--iff-radius); box-shadow: var(--iff-shadow);
|
|
330
|
+
overflow: hidden; animation: iff-in .18s ease-out;
|
|
331
|
+
}
|
|
332
|
+
@keyframes iff-in { from { opacity: 0; transform: translateY(8px) scale(.98); } to { opacity: 1; transform: none; } }
|
|
333
|
+
@media (prefers-reduced-motion: reduce) { .panel { animation: none; } .launcher { transition: none; } }
|
|
334
|
+
.head { display: flex; align-items: flex-start; gap: 10px; padding: 16px 16px 12px; border-bottom: 1px solid var(--iff-border); }
|
|
335
|
+
.head .titles { flex: 1; min-width: 0; }
|
|
336
|
+
.brand { font-family: var(--iff-font-heading); font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--iff-accent); font-weight: 700; }
|
|
337
|
+
.title { margin: 2px 0 0; font-size: 17px; font-weight: 700; line-height: 1.25; }
|
|
338
|
+
.subtitle { margin: 4px 0 0; font-size: 12.5px; color: var(--iff-muted); }
|
|
339
|
+
.icon-btn { flex: none; width: 32px; height: 32px; display: inline-grid; place-items: center; border: none; border-radius: 8px; background: transparent; cursor: pointer; color: var(--iff-muted); }
|
|
340
|
+
.icon-btn:hover { background: var(--iff-hover); color: var(--iff-text); }
|
|
341
|
+
.icon-btn svg { width: 18px; height: 18px; }
|
|
342
|
+
.body { padding: 14px 16px 16px; overflow-y: auto; display: flex; flex-direction: column; gap: 12px; }
|
|
343
|
+
.kinds { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
|
344
|
+
.kind {
|
|
345
|
+
display: flex; flex-direction: column; align-items: flex-start; gap: 6px; padding: 12px; text-align: left; cursor: pointer;
|
|
346
|
+
background: var(--iff-surface); border: 1px solid var(--iff-border); border-radius: var(--iff-radius-sm); transition: border-color .15s ease, background .15s ease;
|
|
347
|
+
}
|
|
348
|
+
.kind:hover { border-color: var(--iff-accent); background: var(--iff-accent-soft); }
|
|
349
|
+
.kind .k-icon { width: 30px; height: 30px; border-radius: 8px; display: inline-grid; place-items: center; background: var(--iff-accent-soft); color: var(--iff-accent); }
|
|
350
|
+
.kind .k-icon svg { width: 18px; height: 18px; }
|
|
351
|
+
.kind strong { font-size: 13.5px; font-weight: 600; }
|
|
352
|
+
.kind span { font-size: 12px; color: var(--iff-muted); }
|
|
353
|
+
.help { display: flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--iff-muted); text-decoration: none; padding: 8px 10px; border-radius: 8px; border: 1px dashed var(--iff-border-strong); }
|
|
354
|
+
.help:hover { color: var(--iff-text); border-color: var(--iff-accent); }
|
|
355
|
+
.help svg { width: 16px; height: 16px; flex: none; }
|
|
356
|
+
.chip { align-self: flex-start; display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; color: var(--iff-accent); background: var(--iff-accent-soft); padding: 4px 10px; border-radius: 9999px; }
|
|
357
|
+
.chip svg { width: 14px; height: 14px; }
|
|
358
|
+
label.field { display: flex; flex-direction: column; gap: 6px; font-size: 12.5px; font-weight: 600; }
|
|
359
|
+
label.field .hint { font-weight: 400; color: var(--iff-muted); }
|
|
360
|
+
input[type="text"], input[type="email"], textarea {
|
|
361
|
+
width: 100%; padding: 10px 12px; border-radius: var(--iff-radius-sm); border: 1px solid var(--iff-border-strong); background: var(--iff-surface);
|
|
362
|
+
font-size: 14px; font-weight: 400;
|
|
363
|
+
}
|
|
364
|
+
textarea { min-height: 120px; resize: vertical; }
|
|
365
|
+
input::placeholder, textarea::placeholder { color: var(--iff-muted); opacity: .9; }
|
|
366
|
+
.row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
|
367
|
+
.meta { display: flex; justify-content: space-between; gap: 8px; font-size: 11.5px; color: var(--iff-muted); }
|
|
368
|
+
.as { font-size: 12px; color: var(--iff-muted); background: var(--iff-surface); border: 1px solid var(--iff-border); border-radius: 8px; padding: 8px 10px; overflow-wrap: anywhere; }
|
|
369
|
+
label.file { display: flex; align-items: center; gap: 8px; font-size: 13px; cursor: pointer; padding: 9px 12px; border: 1px dashed var(--iff-border-strong); border-radius: var(--iff-radius-sm); color: var(--iff-muted); }
|
|
370
|
+
label.file:hover { border-color: var(--iff-accent); color: var(--iff-text); }
|
|
371
|
+
label.file svg { width: 16px; height: 16px; }
|
|
372
|
+
label.file input { position: absolute; width: 1px; height: 1px; opacity: 0; }
|
|
373
|
+
.preview { display: flex; align-items: center; gap: 10px; padding: 8px; border: 1px solid var(--iff-border); border-radius: var(--iff-radius-sm); background: var(--iff-surface); }
|
|
374
|
+
.preview + .preview { margin-top: -6px; }
|
|
375
|
+
.preview img { width: 56px; height: 40px; object-fit: cover; border-radius: 6px; }
|
|
376
|
+
.preview .name { flex: 1; min-width: 0; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
377
|
+
.link-btn { border: none; background: none; color: var(--iff-accent); cursor: pointer; font-size: 12.5px; font-weight: 600; padding: 4px; }
|
|
378
|
+
.privacy { font-size: 11.5px; color: var(--iff-muted); margin: 0; }
|
|
379
|
+
.error { font-size: 12.5px; color: var(--iff-danger); background: var(--iff-danger-soft); border-radius: 8px; padding: 8px 10px; margin: 0; }
|
|
380
|
+
.actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
381
|
+
.primary {
|
|
382
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-width: 120px; height: 40px; padding: 0 18px; border: none; border-radius: var(--iff-radius-sm);
|
|
383
|
+
background: var(--iff-accent); color: var(--iff-accent-contrast); font-weight: 600; cursor: pointer;
|
|
384
|
+
}
|
|
385
|
+
.primary:hover { background: var(--iff-accent-hover); }
|
|
386
|
+
.primary[disabled] { opacity: .65; cursor: progress; }
|
|
387
|
+
.secondary { height: 40px; padding: 0 14px; border-radius: var(--iff-radius-sm); border: 1px solid var(--iff-border-strong); background: transparent; cursor: pointer; font-weight: 600; }
|
|
388
|
+
.secondary:hover { background: var(--iff-hover); }
|
|
389
|
+
.spinner { width: 16px; height: 16px; border-radius: 50%; border: 2px solid rgba(255,255,255,.45); border-top-color: #fff; animation: iff-spin .8s linear infinite; }
|
|
390
|
+
@keyframes iff-spin { to { transform: rotate(360deg); } }
|
|
391
|
+
.done { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 10px; padding: 8px 4px 4px; }
|
|
392
|
+
.done .ok { width: 52px; height: 52px; border-radius: 50%; display: grid; place-items: center; background: var(--iff-success-soft); color: var(--iff-success); }
|
|
393
|
+
.done .ok svg { width: 26px; height: 26px; }
|
|
394
|
+
.done h3 { margin: 0; font-size: 17px; }
|
|
395
|
+
.done p { margin: 0; font-size: 13px; color: var(--iff-muted); }
|
|
396
|
+
.done .folio { font-family: var(--if-font-mono, ui-monospace, monospace); font-weight: 700; color: var(--iff-text); }
|
|
397
|
+
.done .buttons { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; margin-top: 6px; }
|
|
398
|
+
.done a.primary { text-decoration: none; }
|
|
399
|
+
.foot { padding: 8px 16px; border-top: 1px solid var(--iff-border); font-size: 11px; color: var(--iff-muted); text-align: center; }
|
|
400
|
+
@media (max-width: 480px) {
|
|
401
|
+
.panel { inset: 0 !important; width: 100vw; max-width: 100vw; height: 100dvh; max-height: 100dvh; border-radius: 0; border: none;
|
|
402
|
+
padding: var(--if-safe-top, env(safe-area-inset-top, 0px)) var(--if-safe-right, env(safe-area-inset-right, 0px)) var(--if-safe-bottom, env(safe-area-inset-bottom, 0px)) var(--if-safe-left, env(safe-area-inset-left, 0px)); }
|
|
403
|
+
.kinds { grid-template-columns: 1fr; }
|
|
404
|
+
.row { grid-template-columns: 1fr; }
|
|
405
|
+
.launcher .label { display: none; }
|
|
406
|
+
.launcher { width: 48px; height: 48px; padding: 0; justify-content: center; }
|
|
407
|
+
}
|
|
408
|
+
`;
|
|
409
|
+
|
|
410
|
+
// packages/web/feedback-widget/src/widget.ts
|
|
411
|
+
var VERSION = "1.1.1";
|
|
412
|
+
var KINDS = ["bug", "improvement", "question", "praise"];
|
|
413
|
+
var MAX_ATTACHMENT = 5 * 1024 * 1024;
|
|
414
|
+
var MAX_ATTACHMENTS = 5;
|
|
415
|
+
var IMAGE_TYPES = ["image/png", "image/jpeg", "image/webp"];
|
|
416
|
+
var SAFE = {
|
|
417
|
+
bottom: "var(--if-safe-bottom, env(safe-area-inset-bottom, 0px))",
|
|
418
|
+
left: "var(--if-safe-left, env(safe-area-inset-left, 0px))",
|
|
419
|
+
right: "var(--if-safe-right, env(safe-area-inset-right, 0px))"
|
|
420
|
+
};
|
|
421
|
+
var ICONS = {
|
|
422
|
+
launcher: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a8 8 0 0 1-11.6 7.1L4 20l1-4.4A8 8 0 1 1 21 12Z"/><path d="M8.5 11h7M8.5 14.5h4"/></svg>',
|
|
423
|
+
close: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg>',
|
|
424
|
+
back: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg>',
|
|
425
|
+
bug: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 9v4M12 17h.01"/><path d="M10.3 3.9 2.4 17.5A2 2 0 0 0 4.1 20.5h15.8a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z"/></svg>',
|
|
426
|
+
improvement: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 18h6M10 21h4"/><path d="M12 3a6 6 0 0 0-3.6 10.8c.7.5 1.1 1.3 1.1 2.2h5c0-.9.4-1.7 1.1-2.2A6 6 0 0 0 12 3Z"/></svg>',
|
|
427
|
+
question: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M9.5 9.5a2.5 2.5 0 1 1 3.5 2.3c-.6.3-1 .9-1 1.6v.6M12 17h.01"/></svg>',
|
|
428
|
+
praise: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20s-7-4.4-7-10a4 4 0 0 1 7-2.6A4 4 0 0 1 19 10c0 5.6-7 10-7 10Z"/></svg>',
|
|
429
|
+
help: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 18v-6a9 9 0 0 1 18 0v6"/><path d="M21 19a2 2 0 0 1-2 2h-1v-6h3v4ZM3 19a2 2 0 0 0 2 2h1v-6H3v4Z"/></svg>',
|
|
430
|
+
clip: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m21 11-8.6 8.6a5 5 0 0 1-7.1-7.1l8.6-8.6a3.3 3.3 0 0 1 4.7 4.7l-8.6 8.6a1.7 1.7 0 0 1-2.4-2.4l7.9-7.9"/></svg>',
|
|
431
|
+
ok: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5 10 17.5 19 7"/></svg>'
|
|
432
|
+
};
|
|
433
|
+
function el(tag, attrs = {}, ...children) {
|
|
434
|
+
const node = document.createElement(tag);
|
|
435
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
436
|
+
if (value === void 0 || value === false) continue;
|
|
437
|
+
if (key === "class") node.className = String(value);
|
|
438
|
+
else node.setAttribute(key, value === true ? "" : String(value));
|
|
439
|
+
}
|
|
440
|
+
for (const child of children) if (child != null) node.append(child);
|
|
441
|
+
return node;
|
|
442
|
+
}
|
|
443
|
+
function icon(name, className) {
|
|
444
|
+
const span = document.createElement("span");
|
|
445
|
+
if (className) span.className = className;
|
|
446
|
+
span.innerHTML = ICONS[name];
|
|
447
|
+
return span.firstElementChild ? span : span;
|
|
448
|
+
}
|
|
449
|
+
function imageFiles(data, peek = false) {
|
|
450
|
+
const items = Array.from(data?.items ?? []).filter((i) => i.kind === "file" && i.type.startsWith("image/"));
|
|
451
|
+
if (peek) return items.length ? [new File([], "image")] : [];
|
|
452
|
+
return items.map((i) => i.getAsFile()).filter((f) => !!f).map((file) => !file.name || /^image\.\w+$/i.test(file.name) ? new File([file], `captura-pegada.${file.type.split("/")[1] || "png"}`, { type: file.type }) : file);
|
|
453
|
+
}
|
|
454
|
+
function uuid() {
|
|
455
|
+
const c = globalThis.crypto;
|
|
456
|
+
if (c?.randomUUID) return c.randomUUID();
|
|
457
|
+
const bytes = new Uint8Array(16);
|
|
458
|
+
if (c?.getRandomValues) c.getRandomValues(bytes);
|
|
459
|
+
else for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
460
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
461
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
462
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
463
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
464
|
+
}
|
|
465
|
+
var EMAIL = /^[^\s@,;]+@[^\s@,;]+\.[^\s@,;]+$/;
|
|
466
|
+
var IndeFlowFeedbackWidget = class {
|
|
467
|
+
constructor(config) {
|
|
468
|
+
__publicField(this, "config");
|
|
469
|
+
__publicField(this, "remote", null);
|
|
470
|
+
__publicField(this, "host", null);
|
|
471
|
+
__publicField(this, "root", null);
|
|
472
|
+
__publicField(this, "launcherButton", null);
|
|
473
|
+
__publicField(this, "panel", null);
|
|
474
|
+
__publicField(this, "step", "kinds");
|
|
475
|
+
__publicField(this, "kind", null);
|
|
476
|
+
__publicField(this, "draft", { message: "", name: "", email: "" });
|
|
477
|
+
__publicField(this, "attachments", []);
|
|
478
|
+
__publicField(this, "submissionId", uuid());
|
|
479
|
+
__publicField(this, "sending", false);
|
|
480
|
+
__publicField(this, "error", null);
|
|
481
|
+
__publicField(this, "result", null);
|
|
482
|
+
__publicField(this, "extraContext", {});
|
|
483
|
+
__publicField(this, "themeObserver", null);
|
|
484
|
+
__publicField(this, "media", null);
|
|
485
|
+
__publicField(this, "onMedia", () => this.applyTheme());
|
|
486
|
+
__publicField(this, "onKey", (e) => this.handleKey(e));
|
|
487
|
+
__publicField(this, "destroyed", false);
|
|
488
|
+
__publicField(this, "ready");
|
|
489
|
+
this.config = { ...config };
|
|
490
|
+
this.extraContext = { ...config.context ?? {} };
|
|
491
|
+
this.ready = this.start();
|
|
492
|
+
}
|
|
493
|
+
get locale() {
|
|
494
|
+
return resolveLocale(this.config.locale, this.remote?.defaultLocale);
|
|
495
|
+
}
|
|
496
|
+
get t() {
|
|
497
|
+
return COPY[this.locale];
|
|
498
|
+
}
|
|
499
|
+
get endpoint() {
|
|
500
|
+
return `${this.config.apiUrl.replace(/\/+$/, "")}/v1/ingest/feedback`;
|
|
501
|
+
}
|
|
502
|
+
get appName() {
|
|
503
|
+
return this.config.brandLabel || this.remote?.appName || "";
|
|
504
|
+
}
|
|
505
|
+
get user() {
|
|
506
|
+
const u = this.config.user;
|
|
507
|
+
return u && (u.email || u.name || u.externalUserId) ? u : null;
|
|
508
|
+
}
|
|
509
|
+
get anonymous() {
|
|
510
|
+
return !this.user;
|
|
511
|
+
}
|
|
512
|
+
get kinds() {
|
|
513
|
+
const allowed = this.remote?.kinds ?? KINDS;
|
|
514
|
+
const wanted = this.config.kinds?.length ? this.config.kinds : KINDS;
|
|
515
|
+
return KINDS.filter((k) => allowed.includes(k) && wanted.includes(k));
|
|
516
|
+
}
|
|
517
|
+
/** Sin identidad de la app y con anónimos apagados en la fuente, no hay a quién pedirle nada: no se muestra. */
|
|
518
|
+
get available() {
|
|
519
|
+
return !!this.remote && this.kinds.length > 0 && (!this.anonymous || this.remote.allowAnonymous);
|
|
520
|
+
}
|
|
521
|
+
headers(json = false) {
|
|
522
|
+
const h = { "X-IndeFlow-Source-Token": this.config.sourceToken, "Accept-Language": this.locale };
|
|
523
|
+
if (json) h["Content-Type"] = "application/json";
|
|
524
|
+
return h;
|
|
525
|
+
}
|
|
526
|
+
async start() {
|
|
527
|
+
if (typeof document === "undefined" || !this.config.apiUrl || !this.config.sourceToken) return false;
|
|
528
|
+
try {
|
|
529
|
+
const response = await fetch(`${this.endpoint}/config?locale=${encodeURIComponent(this.locale)}`, { headers: this.headers(), credentials: "omit" });
|
|
530
|
+
if (!response.ok) throw new Error(`config ${response.status}`);
|
|
531
|
+
this.remote = await response.json();
|
|
532
|
+
} catch (e) {
|
|
533
|
+
if (typeof console !== "undefined") console.warn("[IndeFlow feedback] widget disabled:", e.message);
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
if (this.destroyed) return false;
|
|
537
|
+
this.mount();
|
|
538
|
+
return true;
|
|
539
|
+
}
|
|
540
|
+
mount() {
|
|
541
|
+
this.host = el("div", { id: "indeflow-feedback", "data-indeflow-feedback": VERSION });
|
|
542
|
+
this.root = this.host.attachShadow({ mode: "open" });
|
|
543
|
+
const style = document.createElement("style");
|
|
544
|
+
style.textContent = STYLES;
|
|
545
|
+
this.root.append(style);
|
|
546
|
+
document.body.append(this.host);
|
|
547
|
+
this.applyColors();
|
|
548
|
+
this.applyTheme();
|
|
549
|
+
this.watchTheme();
|
|
550
|
+
this.render();
|
|
551
|
+
}
|
|
552
|
+
applyColors() {
|
|
553
|
+
if (!this.host) return;
|
|
554
|
+
const map = {
|
|
555
|
+
accent: "--iff-accent",
|
|
556
|
+
accentContrast: "--iff-accent-contrast",
|
|
557
|
+
background: "--iff-bg",
|
|
558
|
+
surface: "--iff-surface",
|
|
559
|
+
text: "--iff-text",
|
|
560
|
+
muted: "--iff-muted",
|
|
561
|
+
border: "--iff-border",
|
|
562
|
+
danger: "--iff-danger",
|
|
563
|
+
success: "--iff-success",
|
|
564
|
+
radius: "--iff-radius",
|
|
565
|
+
font: "--iff-font"
|
|
566
|
+
};
|
|
567
|
+
for (const [key, variable] of Object.entries(map)) {
|
|
568
|
+
const value = this.config.colors?.[key];
|
|
569
|
+
if (value) this.host.style.setProperty(variable, value);
|
|
570
|
+
else this.host.style.removeProperty(variable);
|
|
571
|
+
}
|
|
572
|
+
if (this.config.colors?.accent && !this.config.colors.accentContrast) this.host.style.setProperty("--iff-accent-hover", this.config.colors.accent);
|
|
573
|
+
this.host.style.setProperty("--iff-z", String(this.config.zIndex ?? 2147483e3));
|
|
574
|
+
}
|
|
575
|
+
isDark() {
|
|
576
|
+
const mode = this.config.theme ?? "auto";
|
|
577
|
+
if (mode !== "auto") return mode === "dark";
|
|
578
|
+
const html = document.documentElement;
|
|
579
|
+
if (this.config.themeSelector) {
|
|
580
|
+
try {
|
|
581
|
+
return html.matches(this.config.themeSelector) || document.body.matches(this.config.themeSelector) || !!document.querySelector(this.config.themeSelector);
|
|
582
|
+
} catch {
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
const hint = html.getAttribute("data-theme") ?? document.body.getAttribute("data-theme") ?? html.getAttribute("data-bs-theme");
|
|
586
|
+
if (hint === "dark" || html.classList.contains("dark") || document.body.classList.contains("dark")) return true;
|
|
587
|
+
if (hint === "light" || html.classList.contains("light")) return false;
|
|
588
|
+
return typeof matchMedia !== "undefined" && matchMedia("(prefers-color-scheme: dark)").matches;
|
|
589
|
+
}
|
|
590
|
+
applyTheme() {
|
|
591
|
+
this.host?.setAttribute("data-theme", this.isDark() ? "dark" : "light");
|
|
592
|
+
}
|
|
593
|
+
watchTheme() {
|
|
594
|
+
if (typeof MutationObserver !== "undefined") {
|
|
595
|
+
this.themeObserver = new MutationObserver(() => this.applyTheme());
|
|
596
|
+
const options = { attributes: true, attributeFilter: ["class", "data-theme", "data-bs-theme"] };
|
|
597
|
+
this.themeObserver.observe(document.documentElement, options);
|
|
598
|
+
this.themeObserver.observe(document.body, options);
|
|
599
|
+
}
|
|
600
|
+
if (typeof matchMedia !== "undefined") {
|
|
601
|
+
this.media = matchMedia("(prefers-color-scheme: dark)");
|
|
602
|
+
this.media.addEventListener?.("change", this.onMedia);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
place(node, isPanel) {
|
|
606
|
+
const x = this.config.offset?.x ?? 20, y = this.config.offset?.y ?? 20;
|
|
607
|
+
const left = this.config.position === "bottom-left";
|
|
608
|
+
node.style.position = "fixed";
|
|
609
|
+
node.style.bottom = `calc(${isPanel ? y + 56 : y}px + ${SAFE.bottom})`;
|
|
610
|
+
if (left) {
|
|
611
|
+
node.style.left = `calc(${x}px + ${SAFE.left})`;
|
|
612
|
+
node.style.right = "auto";
|
|
613
|
+
} else {
|
|
614
|
+
node.style.right = `calc(${x}px + ${SAFE.right})`;
|
|
615
|
+
node.style.left = "auto";
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
// ───────────────────────── Render ─────────────────────────
|
|
619
|
+
render() {
|
|
620
|
+
if (!this.root) return;
|
|
621
|
+
const focusedId = this.root.activeElement?.id;
|
|
622
|
+
this.root.querySelectorAll(".launcher, .panel").forEach((n) => n.remove());
|
|
623
|
+
if (!this.available) return;
|
|
624
|
+
if (this.config.launcher !== "none") this.root.append(this.renderLauncher());
|
|
625
|
+
if (this.panel) {
|
|
626
|
+
this.panel = this.renderPanel();
|
|
627
|
+
this.root.append(this.panel);
|
|
628
|
+
const again = focusedId ? this.root.getElementById(focusedId) : null;
|
|
629
|
+
(again ?? this.panel.querySelector("[data-autofocus]"))?.focus();
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
renderLauncher() {
|
|
633
|
+
const label = this.config.launcherLabel || this.t.launcher;
|
|
634
|
+
const button = el("button", { class: this.config.launcherStyle === "icon" ? "launcher icon-only" : "launcher", type: "button", "aria-haspopup": "dialog", "aria-expanded": this.panel ? "true" : "false", title: label, "aria-label": label });
|
|
635
|
+
const type = this.config.launcherIconType ?? "default";
|
|
636
|
+
if (type === "emoji" && this.config.launcherIcon) button.append(el("span", { class: "emoji", "aria-hidden": "true" }, this.config.launcherIcon));
|
|
637
|
+
else if (type === "image" && this.config.launcherIcon) button.append(el("img", { src: this.config.launcherIcon, alt: "" }));
|
|
638
|
+
else if (type === "svg" && this.config.launcherIcon) {
|
|
639
|
+
const s = document.createElement("span");
|
|
640
|
+
s.innerHTML = this.config.launcherIcon;
|
|
641
|
+
button.append(s);
|
|
642
|
+
} else button.append(icon("launcher"));
|
|
643
|
+
button.append(el("span", { class: "label" }, label));
|
|
644
|
+
button.addEventListener("click", () => this.toggle());
|
|
645
|
+
this.place(button, false);
|
|
646
|
+
this.launcherButton = button;
|
|
647
|
+
return button;
|
|
648
|
+
}
|
|
649
|
+
renderPanel() {
|
|
650
|
+
const t = this.t;
|
|
651
|
+
const titleId = "iff-title";
|
|
652
|
+
const panel = el("div", { class: "panel", role: "dialog", "aria-modal": "false", "aria-labelledby": titleId });
|
|
653
|
+
this.place(panel, true);
|
|
654
|
+
const head = el("div", { class: "head" });
|
|
655
|
+
if (this.step === "form") {
|
|
656
|
+
const back = el("button", { class: "icon-btn", type: "button", "aria-label": t.back, title: t.back, id: "iff-back" });
|
|
657
|
+
back.append(icon("back"));
|
|
658
|
+
back.addEventListener("click", () => {
|
|
659
|
+
this.step = "kinds";
|
|
660
|
+
this.error = null;
|
|
661
|
+
this.render();
|
|
662
|
+
});
|
|
663
|
+
head.append(back);
|
|
664
|
+
}
|
|
665
|
+
const titles = el(
|
|
666
|
+
"div",
|
|
667
|
+
{ class: "titles" },
|
|
668
|
+
el("div", { class: "brand" }, this.appName),
|
|
669
|
+
el("h2", { class: "title", id: titleId }, this.config.title || t.title)
|
|
670
|
+
);
|
|
671
|
+
if (this.step === "kinds" && this.appName) titles.append(el("p", { class: "subtitle" }, format(t.subtitle, { app: this.appName })));
|
|
672
|
+
head.append(titles);
|
|
673
|
+
const close = el("button", { class: "icon-btn", type: "button", "aria-label": t.close, title: t.close, id: "iff-close" });
|
|
674
|
+
close.append(icon("close"));
|
|
675
|
+
close.addEventListener("click", () => this.close());
|
|
676
|
+
head.append(close);
|
|
677
|
+
panel.append(head);
|
|
678
|
+
const body = el("div", { class: "body" });
|
|
679
|
+
if (this.step === "kinds") this.renderKinds(body);
|
|
680
|
+
else if (this.step === "form") this.renderForm(body);
|
|
681
|
+
else this.renderDone(body);
|
|
682
|
+
panel.append(body);
|
|
683
|
+
if (this.config.showPoweredBy !== false) panel.append(el("div", { class: "foot" }, t.poweredBy));
|
|
684
|
+
return panel;
|
|
685
|
+
}
|
|
686
|
+
renderKinds(body) {
|
|
687
|
+
const grid = el("div", { class: "kinds" });
|
|
688
|
+
this.kinds.forEach((kind, index) => {
|
|
689
|
+
const copy = this.t.kinds[kind];
|
|
690
|
+
const button = el(
|
|
691
|
+
"button",
|
|
692
|
+
{ class: "kind", type: "button", id: `iff-kind-${kind}`, "data-autofocus": index === 0 },
|
|
693
|
+
icon(kind, "k-icon"),
|
|
694
|
+
el("strong", {}, copy.label),
|
|
695
|
+
el("span", {}, copy.hint)
|
|
696
|
+
);
|
|
697
|
+
button.addEventListener("click", () => this.choose(kind));
|
|
698
|
+
grid.append(button);
|
|
699
|
+
});
|
|
700
|
+
body.append(grid);
|
|
701
|
+
if (this.config.supportUrl) {
|
|
702
|
+
const link = el("a", { class: "help", href: this.config.supportUrl, target: this.config.supportUrl.startsWith("/") ? void 0 : "_blank", rel: "noopener" });
|
|
703
|
+
link.append(icon("help"), el("span", {}, this.config.supportLabel || this.t.needHelp));
|
|
704
|
+
body.append(link);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
renderForm(body) {
|
|
708
|
+
const t = this.t;
|
|
709
|
+
const kind = this.kind;
|
|
710
|
+
const chip = el("span", { class: "chip" });
|
|
711
|
+
chip.append(icon(kind), t.kinds[kind].label);
|
|
712
|
+
body.append(chip);
|
|
713
|
+
const max = this.remote?.maxMessage ?? 4e3;
|
|
714
|
+
const textarea = el("textarea", { id: "iff-message", maxlength: String(max), placeholder: t.kinds[kind].placeholder, "data-autofocus": true, "aria-describedby": "iff-counter" });
|
|
715
|
+
textarea.value = this.draft.message;
|
|
716
|
+
const counter = el("span", { id: "iff-counter" }, format(t.counter, { n: this.draft.message.length, max }));
|
|
717
|
+
textarea.addEventListener("input", () => {
|
|
718
|
+
this.draft.message = textarea.value;
|
|
719
|
+
counter.textContent = format(t.counter, { n: textarea.value.length, max });
|
|
720
|
+
});
|
|
721
|
+
if (this.config.screenshot !== false) {
|
|
722
|
+
textarea.addEventListener("paste", (e) => {
|
|
723
|
+
const files = imageFiles(e.clipboardData);
|
|
724
|
+
if (files.length) {
|
|
725
|
+
e.preventDefault();
|
|
726
|
+
void this.attach(files);
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
textarea.addEventListener("dragover", (e) => {
|
|
730
|
+
if (imageFiles(e.dataTransfer, true).length) e.preventDefault();
|
|
731
|
+
});
|
|
732
|
+
textarea.addEventListener("drop", (e) => {
|
|
733
|
+
const files = imageFiles(e.dataTransfer);
|
|
734
|
+
if (files.length) {
|
|
735
|
+
e.preventDefault();
|
|
736
|
+
void this.attach(files);
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
body.append(el("label", { class: "field", for: "iff-message" }, t.message, textarea), el("div", { class: "meta" }, el("span", {}, ""), counter));
|
|
741
|
+
if (this.anonymous) {
|
|
742
|
+
const name = el("input", { id: "iff-name", type: "text", maxlength: "100", autocomplete: "name" });
|
|
743
|
+
name.value = this.draft.name;
|
|
744
|
+
name.addEventListener("input", () => this.draft.name = name.value);
|
|
745
|
+
const email = el("input", { id: "iff-email", type: "email", maxlength: "254", autocomplete: "email", inputmode: "email" });
|
|
746
|
+
email.value = this.draft.email;
|
|
747
|
+
email.addEventListener("input", () => this.draft.email = email.value);
|
|
748
|
+
body.append(el("div", { class: "row" }, el("label", { class: "field", for: "iff-name" }, t.name, name), el("label", { class: "field", for: "iff-email" }, t.email, email)));
|
|
749
|
+
} else {
|
|
750
|
+
const u = this.user;
|
|
751
|
+
if (!u.email) {
|
|
752
|
+
const email = el("input", { id: "iff-email", type: "email", maxlength: "254", autocomplete: "email" });
|
|
753
|
+
email.value = this.draft.email;
|
|
754
|
+
email.addEventListener("input", () => this.draft.email = email.value);
|
|
755
|
+
body.append(el("label", { class: "field", for: "iff-email" }, el("span", {}, t.email, " ", el("span", { class: "hint" }, this.remote?.requireEmail ? "" : `\xB7 ${t.emailOptional}`)), email));
|
|
756
|
+
}
|
|
757
|
+
const who = [u.name, u.email].filter(Boolean).join(" \xB7 ");
|
|
758
|
+
if (who) body.append(el("div", { class: "as" }, format(t.sendingAs, { who })));
|
|
759
|
+
}
|
|
760
|
+
if (this.config.screenshot !== false) {
|
|
761
|
+
const limit = this.maxAttachments();
|
|
762
|
+
this.attachments.forEach((a, i) => {
|
|
763
|
+
const remove = el("button", { class: "link-btn", type: "button", id: `iff-remove-${i}`, "data-remove": String(i) }, t.remove);
|
|
764
|
+
remove.addEventListener("click", () => {
|
|
765
|
+
this.attachments.splice(i, 1);
|
|
766
|
+
this.error = null;
|
|
767
|
+
this.render();
|
|
768
|
+
});
|
|
769
|
+
body.append(el("div", { class: "preview" }, el("img", { src: a.dataUrl, alt: "" }), el("span", { class: "name" }, a.name), remove));
|
|
770
|
+
});
|
|
771
|
+
if (this.attachments.length < limit) {
|
|
772
|
+
const input = el("input", { id: "iff-file", type: "file", accept: IMAGE_TYPES.join(","), multiple: true });
|
|
773
|
+
input.addEventListener("change", () => {
|
|
774
|
+
const files = Array.from(input.files ?? []);
|
|
775
|
+
if (files.length) void this.attach(files);
|
|
776
|
+
});
|
|
777
|
+
const label = el("label", { class: "file", for: "iff-file" });
|
|
778
|
+
const hint = format(t.attachHint, { max: limit, mb: this.maxMegabytes() });
|
|
779
|
+
label.append(icon("clip"), el("span", {}, this.attachments.length ? t.attachMore : t.attach, " ", el("span", { class: "hint" }, `\xB7 ${hint}`)), input);
|
|
780
|
+
body.append(label);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
body.append(el("p", { class: "privacy" }, t.privacy));
|
|
784
|
+
if (this.error) body.append(el("p", { class: "error", role: "alert" }, this.error));
|
|
785
|
+
const send = el("button", { class: "primary", type: "button", id: "iff-send", disabled: this.sending, "aria-busy": this.sending ? "true" : void 0 });
|
|
786
|
+
if (this.sending) send.append(el("span", { class: "spinner", "aria-hidden": "true" }), t.sending);
|
|
787
|
+
else send.append(t.send);
|
|
788
|
+
send.addEventListener("click", () => void this.submit());
|
|
789
|
+
body.append(el("div", { class: "actions" }, send));
|
|
790
|
+
}
|
|
791
|
+
renderDone(body) {
|
|
792
|
+
const t = this.t;
|
|
793
|
+
const r = this.result;
|
|
794
|
+
const ok = el("div", { class: "ok" });
|
|
795
|
+
ok.append(icon("ok"));
|
|
796
|
+
const text = el("p", {});
|
|
797
|
+
const parts = t.successBody.split("{folio}");
|
|
798
|
+
text.append(parts[0], el("span", { class: "folio" }, r.folio), parts[1] ?? "");
|
|
799
|
+
const done = el("div", { class: "done" }, ok, el("h3", {}, t.successTitle), text);
|
|
800
|
+
const email = this.user?.email || this.draft.email;
|
|
801
|
+
if (email) done.append(el("p", {}, format(t.successEmail, { email })));
|
|
802
|
+
const status = el("a", { class: "primary", href: r.trackingUrl, target: "_blank", rel: "noopener", id: "iff-status" }, t.viewStatus);
|
|
803
|
+
const another = el("button", { class: "secondary", type: "button", id: "iff-another", "data-autofocus": true }, t.another);
|
|
804
|
+
another.addEventListener("click", () => this.reset());
|
|
805
|
+
done.append(el("div", { class: "buttons" }, status, another));
|
|
806
|
+
body.append(done);
|
|
807
|
+
}
|
|
808
|
+
// ───────────────────────── Acciones ─────────────────────────
|
|
809
|
+
choose(kind) {
|
|
810
|
+
this.kind = kind;
|
|
811
|
+
this.step = "form";
|
|
812
|
+
this.error = null;
|
|
813
|
+
this.render();
|
|
814
|
+
}
|
|
815
|
+
reset() {
|
|
816
|
+
this.step = "kinds";
|
|
817
|
+
this.kind = null;
|
|
818
|
+
this.draft = { message: "", name: this.draft.name, email: this.draft.email };
|
|
819
|
+
this.attachments = [];
|
|
820
|
+
this.result = null;
|
|
821
|
+
this.error = null;
|
|
822
|
+
this.submissionId = uuid();
|
|
823
|
+
this.render();
|
|
824
|
+
}
|
|
825
|
+
maxAttachments() {
|
|
826
|
+
return Math.max(1, this.remote?.maxAttachments ?? MAX_ATTACHMENTS);
|
|
827
|
+
}
|
|
828
|
+
maxMegabytes() {
|
|
829
|
+
return Math.max(1, Math.round((this.remote?.maxAttachmentBytes ?? MAX_ATTACHMENT) / 1048576));
|
|
830
|
+
}
|
|
831
|
+
/** Adds each valid image up to the limit; the ones that fail leave their message and the rest still get attached. */
|
|
832
|
+
async attach(files) {
|
|
833
|
+
const limit = this.maxAttachments();
|
|
834
|
+
const maxBytes = this.remote?.maxAttachmentBytes ?? MAX_ATTACHMENT;
|
|
835
|
+
this.error = null;
|
|
836
|
+
for (const file of files) {
|
|
837
|
+
if (this.attachments.length >= limit) {
|
|
838
|
+
this.error = format(this.t.errorAttachmentCount, { max: limit });
|
|
839
|
+
break;
|
|
840
|
+
}
|
|
841
|
+
if (!IMAGE_TYPES.includes(file.type)) {
|
|
842
|
+
this.error = this.t.errorAttachmentType;
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
if (file.size > maxBytes) {
|
|
846
|
+
this.error = format(this.t.errorAttachmentSize, { mb: this.maxMegabytes() });
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
const dataUrl = await new Promise((resolve, reject) => {
|
|
850
|
+
const reader = new FileReader();
|
|
851
|
+
reader.onload = () => resolve(String(reader.result));
|
|
852
|
+
reader.onerror = () => reject(reader.error);
|
|
853
|
+
reader.readAsDataURL(file);
|
|
854
|
+
});
|
|
855
|
+
this.attachments.push({ name: file.name || "captura.png", type: file.type, dataUrl, size: file.size });
|
|
856
|
+
}
|
|
857
|
+
this.render();
|
|
858
|
+
}
|
|
859
|
+
validate() {
|
|
860
|
+
const t = this.t;
|
|
861
|
+
if (this.draft.message.trim().length < 3) return t.errorMessage;
|
|
862
|
+
if (this.anonymous) {
|
|
863
|
+
if (!this.draft.name.trim()) return t.errorName;
|
|
864
|
+
if (!EMAIL.test(this.draft.email.trim())) return t.errorEmail;
|
|
865
|
+
} else if (!this.user.email) {
|
|
866
|
+
const email = this.draft.email.trim();
|
|
867
|
+
if (email && !EMAIL.test(email)) return t.errorEmail;
|
|
868
|
+
if (!email && this.remote?.requireEmail) return t.errorEmail;
|
|
869
|
+
}
|
|
870
|
+
return null;
|
|
871
|
+
}
|
|
872
|
+
context() {
|
|
873
|
+
const loc = typeof location !== "undefined" ? location : null;
|
|
874
|
+
const extra = {};
|
|
875
|
+
for (const [k, v] of Object.entries(this.extraContext).slice(0, 20)) if (v != null) extra[k] = String(v).slice(0, 200);
|
|
876
|
+
return {
|
|
877
|
+
url: loc ? `${loc.origin}${loc.pathname}` : void 0,
|
|
878
|
+
route: this.config.collectRoute === false || !loc ? void 0 : loc.pathname,
|
|
879
|
+
appVersion: this.config.appVersion,
|
|
880
|
+
browser: typeof navigator !== "undefined" ? navigator.userAgent.slice(0, 200) : void 0,
|
|
881
|
+
viewport: typeof innerWidth !== "undefined" ? `${innerWidth}x${innerHeight}` : void 0,
|
|
882
|
+
theme: this.host?.getAttribute("data-theme") ?? void 0,
|
|
883
|
+
extra
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
async submit() {
|
|
887
|
+
if (this.sending || !this.kind) return;
|
|
888
|
+
const invalid = this.validate();
|
|
889
|
+
if (invalid) {
|
|
890
|
+
this.error = invalid;
|
|
891
|
+
this.render();
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
this.sending = true;
|
|
895
|
+
this.error = null;
|
|
896
|
+
this.render();
|
|
897
|
+
try {
|
|
898
|
+
const challengeResponse = await fetch(`${this.endpoint}/challenge`, { headers: this.headers(), credentials: "omit" });
|
|
899
|
+
const challenge = await challengeResponse.json().catch(() => ({}));
|
|
900
|
+
if (!challengeResponse.ok) throw Object.assign(new Error(challenge.message || this.t.errorGeneric), { code: challenge.code || "UNAVAILABLE" });
|
|
901
|
+
const proof = await solveChallenge(challenge.token, challenge.difficulty ?? 4);
|
|
902
|
+
const u = this.user;
|
|
903
|
+
const body = {
|
|
904
|
+
clientSubmissionId: this.submissionId,
|
|
905
|
+
kind: this.kind,
|
|
906
|
+
message: this.draft.message.trim(),
|
|
907
|
+
locale: this.locale,
|
|
908
|
+
reporter: u ? { mode: "app", name: u.name, email: u.email || this.draft.email.trim() || void 0, company: u.company, externalUserId: u.externalUserId } : { mode: "anonymous", name: this.draft.name.trim(), email: this.draft.email.trim() },
|
|
909
|
+
context: this.context(),
|
|
910
|
+
// «attachments» lleva todas; «attachment» (la primera) sigue para un servidor anterior a FB-000005.
|
|
911
|
+
attachments: this.attachments.map((a) => ({ name: a.name, contentType: a.type, base64: a.dataUrl })),
|
|
912
|
+
attachment: this.attachments[0] ? { name: this.attachments[0].name, contentType: this.attachments[0].type, base64: this.attachments[0].dataUrl } : void 0,
|
|
913
|
+
challenge: challenge.token,
|
|
914
|
+
proof
|
|
915
|
+
};
|
|
916
|
+
const response = await fetch(this.endpoint, { method: "POST", headers: this.headers(true), body: JSON.stringify(body), credentials: "omit" });
|
|
917
|
+
const data = await response.json().catch(() => ({}));
|
|
918
|
+
if (!response.ok) throw Object.assign(new Error(data.message || this.t.errorGeneric), { code: data.code || `HTTP_${response.status}` });
|
|
919
|
+
this.result = { folio: data.folio, trackingUrl: data.trackingUrl };
|
|
920
|
+
this.step = "done";
|
|
921
|
+
this.config.onSubmitted?.(this.result);
|
|
922
|
+
} catch (e) {
|
|
923
|
+
const err = e;
|
|
924
|
+
this.error = err.code ? err.message : this.t.errorGeneric;
|
|
925
|
+
this.config.onError?.({ code: err.code ?? "NETWORK", message: this.error });
|
|
926
|
+
} finally {
|
|
927
|
+
this.sending = false;
|
|
928
|
+
this.render();
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
handleKey(e) {
|
|
932
|
+
if (!this.panel || !this.root) return;
|
|
933
|
+
if (e.key === "Escape") {
|
|
934
|
+
e.stopPropagation();
|
|
935
|
+
this.close();
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
if (e.key !== "Tab") return;
|
|
939
|
+
const focusables = Array.from(this.panel.querySelectorAll("button:not([disabled]), a[href], input, textarea")).filter((n) => n.offsetParent !== null || n === this.root.activeElement);
|
|
940
|
+
if (focusables.length === 0) return;
|
|
941
|
+
const first = focusables[0], last = focusables[focusables.length - 1];
|
|
942
|
+
const active = this.root.activeElement;
|
|
943
|
+
if (e.shiftKey && active === first) {
|
|
944
|
+
e.preventDefault();
|
|
945
|
+
last.focus();
|
|
946
|
+
} else if (!e.shiftKey && active === last) {
|
|
947
|
+
e.preventDefault();
|
|
948
|
+
first.focus();
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
// ───────────────────────── API pública ─────────────────────────
|
|
952
|
+
open(kind) {
|
|
953
|
+
if (!this.root || !this.available) return;
|
|
954
|
+
if (!this.panel) {
|
|
955
|
+
this.panel = document.createElement("div");
|
|
956
|
+
this.host.addEventListener("keydown", this.onKey);
|
|
957
|
+
}
|
|
958
|
+
if (kind && this.kinds.includes(kind)) {
|
|
959
|
+
this.kind = kind;
|
|
960
|
+
this.step = "form";
|
|
961
|
+
}
|
|
962
|
+
this.render();
|
|
963
|
+
}
|
|
964
|
+
close() {
|
|
965
|
+
if (!this.panel) return;
|
|
966
|
+
this.panel = null;
|
|
967
|
+
this.host?.removeEventListener("keydown", this.onKey);
|
|
968
|
+
if (this.step === "done") this.reset();
|
|
969
|
+
else this.render();
|
|
970
|
+
this.root?.querySelector(".launcher")?.focus();
|
|
971
|
+
}
|
|
972
|
+
toggle() {
|
|
973
|
+
if (this.panel) this.close();
|
|
974
|
+
else this.open();
|
|
975
|
+
}
|
|
976
|
+
setUser(user) {
|
|
977
|
+
this.config.user = user;
|
|
978
|
+
this.render();
|
|
979
|
+
}
|
|
980
|
+
setContext(context) {
|
|
981
|
+
this.extraContext = { ...context };
|
|
982
|
+
}
|
|
983
|
+
update(config) {
|
|
984
|
+
this.config = { ...this.config, ...config };
|
|
985
|
+
if (config.context) this.extraContext = { ...config.context };
|
|
986
|
+
this.applyColors();
|
|
987
|
+
this.applyTheme();
|
|
988
|
+
this.render();
|
|
989
|
+
}
|
|
990
|
+
destroy() {
|
|
991
|
+
this.destroyed = true;
|
|
992
|
+
this.themeObserver?.disconnect();
|
|
993
|
+
this.media?.removeEventListener?.("change", this.onMedia);
|
|
994
|
+
this.host?.remove();
|
|
995
|
+
this.host = null;
|
|
996
|
+
this.root = null;
|
|
997
|
+
this.panel = null;
|
|
998
|
+
this.launcherButton = null;
|
|
999
|
+
}
|
|
1000
|
+
};
|
|
1001
|
+
function createIndeFlowFeedback(config) {
|
|
1002
|
+
return new IndeFlowFeedbackWidget(config);
|
|
1003
|
+
}
|
|
1004
|
+
export {
|
|
1005
|
+
COPY,
|
|
1006
|
+
IndeFlowFeedbackWidget,
|
|
1007
|
+
VERSION,
|
|
1008
|
+
createIndeFlowFeedback,
|
|
1009
|
+
resolveLocale,
|
|
1010
|
+
sha256Hex,
|
|
1011
|
+
solveChallenge
|
|
1012
|
+
};
|