@omg-dev/sdk 0.4.29 → 0.4.31
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/OmgBadge-C559Dp1f.mjs +222 -0
- package/dist/brand/auto.mjs +1 -2
- package/dist/index.mjs +203 -23
- package/package.json +2 -7
- package/src/brand/OmgBadge.tsx +10 -194
- package/src/brand/auto.tsx +0 -9
- package/src/index.ts +0 -3
- package/src/sandbox.test.ts +4 -0
- package/src/sandbox.ts +1 -0
- package/dist/OmgBadge-CDnTIlnn.mjs +0 -371
- package/dist/VibesFeedback-BF2Vf6FK.mjs +0 -808
- package/dist/feedback/auto.mjs +0 -26
- package/src/feedback/VibesFeedback.tsx +0 -360
- package/src/feedback/auto.tsx +0 -47
- package/src/feedback/gestures.ts +0 -296
- package/src/feedback/index.ts +0 -18
- package/src/feedback/screenshot.ts +0 -37
- package/src/feedback/trace.ts +0 -166
|
@@ -1,808 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
//#region src/auth/bridge.ts
|
|
4
|
-
let current = {
|
|
5
|
-
user: null,
|
|
6
|
-
token: null,
|
|
7
|
-
authReady: false
|
|
8
|
-
};
|
|
9
|
-
const authChangeListeners = /* @__PURE__ */ new Set();
|
|
10
|
-
function setAuthContext(snap) {
|
|
11
|
-
current = snap;
|
|
12
|
-
for (const cb of authChangeListeners) cb(snap);
|
|
13
|
-
}
|
|
14
|
-
function getAuthContext() {
|
|
15
|
-
return current;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Subscribe to auth snapshot changes (user / token / authReady). Returns an
|
|
19
|
-
* unsubscribe fn. Data hooks use this to re-establish a subscription when the
|
|
20
|
-
* JWT finishes loading (null→token) or on sign-out (token→null) — without it
|
|
21
|
-
* the WS connects once, often before getToken() resolves, and never retries
|
|
22
|
-
* with a bearer.
|
|
23
|
-
*/
|
|
24
|
-
function subscribeAuthChange(cb) {
|
|
25
|
-
authChangeListeners.add(cb);
|
|
26
|
-
return () => authChangeListeners.delete(cb);
|
|
27
|
-
}
|
|
28
|
-
const authRequiredListeners = /* @__PURE__ */ new Set();
|
|
29
|
-
/** Subscribe to auth-required signals. Returns an unsubscribe fn. */
|
|
30
|
-
function subscribeAuthRequired(cb) {
|
|
31
|
-
authRequiredListeners.add(cb);
|
|
32
|
-
return () => authRequiredListeners.delete(cb);
|
|
33
|
-
}
|
|
34
|
-
/** Notify all subscribers that a request hit the server's auth-required signal. */
|
|
35
|
-
function notifyAuthRequired() {
|
|
36
|
-
for (const cb of authRequiredListeners) cb();
|
|
37
|
-
}
|
|
38
|
-
//#endregion
|
|
39
|
-
//#region src/storage/useUpload.ts
|
|
40
|
-
function bearerHeaders() {
|
|
41
|
-
const { token } = getAuthContext();
|
|
42
|
-
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Headless upload hook. Returns the upload function plus state.
|
|
46
|
-
*
|
|
47
|
-
* Usage:
|
|
48
|
-
* const { upload, uploading, progress, error } = useUpload()
|
|
49
|
-
* await upload(file, { key: `avatars/${user.id}.png` })
|
|
50
|
-
*/
|
|
51
|
-
function useUpload() {
|
|
52
|
-
const [state, setState] = useState({
|
|
53
|
-
uploading: false,
|
|
54
|
-
progress: 0,
|
|
55
|
-
error: null,
|
|
56
|
-
result: null
|
|
57
|
-
});
|
|
58
|
-
const reset = useCallback(() => {
|
|
59
|
-
setState({
|
|
60
|
-
uploading: false,
|
|
61
|
-
progress: 0,
|
|
62
|
-
error: null,
|
|
63
|
-
result: null
|
|
64
|
-
});
|
|
65
|
-
}, []);
|
|
66
|
-
const upload = useCallback(async (file, opts) => {
|
|
67
|
-
setState({
|
|
68
|
-
uploading: true,
|
|
69
|
-
progress: 0,
|
|
70
|
-
error: null,
|
|
71
|
-
result: null
|
|
72
|
-
});
|
|
73
|
-
const presignUrl = opts.presignUrl ?? "/api/_storage/presign";
|
|
74
|
-
const contentType = opts.contentType ?? file.type ?? "application/octet-stream";
|
|
75
|
-
try {
|
|
76
|
-
const presignRes = await fetch(presignUrl, {
|
|
77
|
-
method: "POST",
|
|
78
|
-
headers: {
|
|
79
|
-
"Content-Type": "application/json",
|
|
80
|
-
...bearerHeaders()
|
|
81
|
-
},
|
|
82
|
-
body: JSON.stringify({
|
|
83
|
-
action: "put",
|
|
84
|
-
key: opts.key,
|
|
85
|
-
contentType,
|
|
86
|
-
scope: opts.scope ?? "user"
|
|
87
|
-
})
|
|
88
|
-
});
|
|
89
|
-
if (!presignRes.ok) {
|
|
90
|
-
if (presignRes.status === 401 && getAuthContext().authReady) notifyAuthRequired();
|
|
91
|
-
const text = await presignRes.text().catch(() => "");
|
|
92
|
-
throw new Error(`presign ${presignRes.status}: ${text.slice(0, 200)}`);
|
|
93
|
-
}
|
|
94
|
-
const { url, key: storedKey, maxBytes } = await presignRes.json();
|
|
95
|
-
if (typeof maxBytes === "number" && file.size > maxBytes) throw new Error(`file too large (${file.size} > ${maxBytes} bytes)`);
|
|
96
|
-
await new Promise((resolve, reject) => {
|
|
97
|
-
const xhr = new XMLHttpRequest();
|
|
98
|
-
xhr.open("PUT", url);
|
|
99
|
-
xhr.setRequestHeader("Content-Type", contentType);
|
|
100
|
-
xhr.upload.onprogress = (e) => {
|
|
101
|
-
if (e.lengthComputable) {
|
|
102
|
-
const frac = e.loaded / e.total;
|
|
103
|
-
setState((s) => ({
|
|
104
|
-
...s,
|
|
105
|
-
progress: frac
|
|
106
|
-
}));
|
|
107
|
-
opts.onProgress?.(frac);
|
|
108
|
-
}
|
|
109
|
-
};
|
|
110
|
-
xhr.onload = () => {
|
|
111
|
-
if (xhr.status >= 200 && xhr.status < 300) resolve();
|
|
112
|
-
else reject(/* @__PURE__ */ new Error(`PUT ${xhr.status}: ${xhr.responseText.slice(0, 200)}`));
|
|
113
|
-
};
|
|
114
|
-
xhr.onerror = () => reject(/* @__PURE__ */ new Error("network error during upload"));
|
|
115
|
-
xhr.send(file);
|
|
116
|
-
});
|
|
117
|
-
const dlRes = await fetch(presignUrl, {
|
|
118
|
-
method: "POST",
|
|
119
|
-
headers: {
|
|
120
|
-
"Content-Type": "application/json",
|
|
121
|
-
...bearerHeaders()
|
|
122
|
-
},
|
|
123
|
-
body: JSON.stringify({
|
|
124
|
-
action: "get",
|
|
125
|
-
key: opts.key,
|
|
126
|
-
scope: opts.scope ?? "user"
|
|
127
|
-
})
|
|
128
|
-
});
|
|
129
|
-
let downloadUrl = "";
|
|
130
|
-
if (dlRes.ok) downloadUrl = (await dlRes.json()).url ?? "";
|
|
131
|
-
fetch(presignUrl.replace(/\/presign$/, "/notify"), {
|
|
132
|
-
method: "POST",
|
|
133
|
-
headers: {
|
|
134
|
-
"Content-Type": "application/json",
|
|
135
|
-
...bearerHeaders()
|
|
136
|
-
},
|
|
137
|
-
body: JSON.stringify({
|
|
138
|
-
action: "upload",
|
|
139
|
-
key: opts.key,
|
|
140
|
-
size: file.size,
|
|
141
|
-
contentType,
|
|
142
|
-
scope: opts.scope ?? "user"
|
|
143
|
-
})
|
|
144
|
-
}).catch(() => {});
|
|
145
|
-
const result = {
|
|
146
|
-
key: storedKey,
|
|
147
|
-
downloadUrl,
|
|
148
|
-
size: file.size,
|
|
149
|
-
contentType
|
|
150
|
-
};
|
|
151
|
-
setState({
|
|
152
|
-
uploading: false,
|
|
153
|
-
progress: 1,
|
|
154
|
-
error: null,
|
|
155
|
-
result
|
|
156
|
-
});
|
|
157
|
-
return result;
|
|
158
|
-
} catch (err) {
|
|
159
|
-
setState({
|
|
160
|
-
uploading: false,
|
|
161
|
-
progress: 0,
|
|
162
|
-
error: err.message ?? String(err),
|
|
163
|
-
result: null
|
|
164
|
-
});
|
|
165
|
-
throw err;
|
|
166
|
-
}
|
|
167
|
-
}, []);
|
|
168
|
-
return {
|
|
169
|
-
...state,
|
|
170
|
-
upload,
|
|
171
|
-
reset
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
//#endregion
|
|
175
|
-
//#region src/feedback/gestures.ts
|
|
176
|
-
/** Default gestures when the host doesn't specify any. Permission-free and
|
|
177
|
-
* OS-gesture-safe — notably excludes "shake" (see the note above). */
|
|
178
|
-
const DEFAULT_GESTURES = ["two-finger-press"];
|
|
179
|
-
const DEFAULTS = {
|
|
180
|
-
edgeSize: 28,
|
|
181
|
-
swipeThreshold: 90,
|
|
182
|
-
swipeMaxDuration: 600,
|
|
183
|
-
shakeThreshold: 24,
|
|
184
|
-
pressDuration: 450,
|
|
185
|
-
cooldown: 1200
|
|
186
|
-
};
|
|
187
|
-
function hasWindow() {
|
|
188
|
-
return typeof window !== "undefined" && typeof document !== "undefined";
|
|
189
|
-
}
|
|
190
|
-
function motionPermissionState() {
|
|
191
|
-
if (!hasWindow() || typeof window.DeviceMotionEvent === "undefined") return "unsupported";
|
|
192
|
-
return typeof window.DeviceMotionEvent.requestPermission === "function" ? "prompt" : "granted";
|
|
193
|
-
}
|
|
194
|
-
/**
|
|
195
|
-
* Ask for accelerometer access (iOS 13+). MUST be called from inside a user
|
|
196
|
-
* gesture handler (tap), or iOS rejects it. No-op elsewhere (returns true).
|
|
197
|
-
*/
|
|
198
|
-
async function requestMotionPermission() {
|
|
199
|
-
const state = motionPermissionState();
|
|
200
|
-
if (state === "granted") return true;
|
|
201
|
-
if (state === "unsupported") return false;
|
|
202
|
-
const ctor = window.DeviceMotionEvent;
|
|
203
|
-
try {
|
|
204
|
-
return await ctor.requestPermission() === "granted";
|
|
205
|
-
} catch {
|
|
206
|
-
return false;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
/**
|
|
210
|
-
* Attach gesture listeners to the document. Returns a teardown function.
|
|
211
|
-
* `onTrigger` fires (debounced by `cooldown`) whenever any armed gesture
|
|
212
|
-
* completes. Pure DOM — usable without React.
|
|
213
|
-
*/
|
|
214
|
-
function attachGestureListeners(onTrigger, options = {}) {
|
|
215
|
-
if (!hasWindow()) return () => {};
|
|
216
|
-
const opts = {
|
|
217
|
-
...DEFAULTS,
|
|
218
|
-
...options
|
|
219
|
-
};
|
|
220
|
-
const gestures = new Set(options.gestures ?? DEFAULT_GESTURES);
|
|
221
|
-
const cleanups = [];
|
|
222
|
-
let lastTrigger = 0;
|
|
223
|
-
function fire() {
|
|
224
|
-
const now = Date.now();
|
|
225
|
-
if (now - lastTrigger < opts.cooldown) return;
|
|
226
|
-
lastTrigger = now;
|
|
227
|
-
try {
|
|
228
|
-
navigator.vibrate?.(12);
|
|
229
|
-
} catch {}
|
|
230
|
-
onTrigger();
|
|
231
|
-
}
|
|
232
|
-
if (gestures.has("swipe-up") || gestures.has("swipe-down") || gestures.has("swipe-left") || gestures.has("swipe-right")) {
|
|
233
|
-
let sx = 0;
|
|
234
|
-
let sy = 0;
|
|
235
|
-
let st = 0;
|
|
236
|
-
let fromEdge = null;
|
|
237
|
-
const onStart = (e) => {
|
|
238
|
-
if (e.touches.length !== 1) {
|
|
239
|
-
fromEdge = null;
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
const t = e.touches[0];
|
|
243
|
-
sx = t.clientX;
|
|
244
|
-
sy = t.clientY;
|
|
245
|
-
st = Date.now();
|
|
246
|
-
const w = window.innerWidth;
|
|
247
|
-
const h = window.innerHeight;
|
|
248
|
-
fromEdge = gestures.has("swipe-down") && sy <= opts.edgeSize ? "top" : gestures.has("swipe-up") && sy >= h - opts.edgeSize ? "bottom" : gestures.has("swipe-right") && sx <= opts.edgeSize ? "left" : gestures.has("swipe-left") && sx >= w - opts.edgeSize ? "right" : null;
|
|
249
|
-
};
|
|
250
|
-
const onEnd = (e) => {
|
|
251
|
-
if (!fromEdge) return;
|
|
252
|
-
const edge = fromEdge;
|
|
253
|
-
fromEdge = null;
|
|
254
|
-
const t = e.changedTouches[0];
|
|
255
|
-
if (!t) return;
|
|
256
|
-
const dx = t.clientX - sx;
|
|
257
|
-
const dy = t.clientY - sy;
|
|
258
|
-
if (Date.now() - st > opts.swipeMaxDuration) return;
|
|
259
|
-
if (edge === "bottom" && -dy >= opts.swipeThreshold && Math.abs(dy) > Math.abs(dx) || edge === "top" && dy >= opts.swipeThreshold && Math.abs(dy) > Math.abs(dx) || edge === "right" && -dx >= opts.swipeThreshold && Math.abs(dx) > Math.abs(dy) || edge === "left" && dx >= opts.swipeThreshold && Math.abs(dx) > Math.abs(dy)) fire();
|
|
260
|
-
};
|
|
261
|
-
document.addEventListener("touchstart", onStart, { passive: true });
|
|
262
|
-
document.addEventListener("touchend", onEnd, { passive: true });
|
|
263
|
-
cleanups.push(() => {
|
|
264
|
-
document.removeEventListener("touchstart", onStart);
|
|
265
|
-
document.removeEventListener("touchend", onEnd);
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
if (gestures.has("two-finger-press")) {
|
|
269
|
-
let timer = null;
|
|
270
|
-
const clear = () => {
|
|
271
|
-
if (timer) {
|
|
272
|
-
clearTimeout(timer);
|
|
273
|
-
timer = null;
|
|
274
|
-
}
|
|
275
|
-
};
|
|
276
|
-
const onStart = (e) => {
|
|
277
|
-
if (e.touches.length === 2) {
|
|
278
|
-
clear();
|
|
279
|
-
timer = setTimeout(fire, opts.pressDuration);
|
|
280
|
-
} else clear();
|
|
281
|
-
};
|
|
282
|
-
document.addEventListener("touchstart", onStart, { passive: true });
|
|
283
|
-
document.addEventListener("touchend", clear, { passive: true });
|
|
284
|
-
document.addEventListener("touchmove", clear, { passive: true });
|
|
285
|
-
document.addEventListener("touchcancel", clear, { passive: true });
|
|
286
|
-
cleanups.push(() => {
|
|
287
|
-
clear();
|
|
288
|
-
document.removeEventListener("touchstart", onStart);
|
|
289
|
-
document.removeEventListener("touchend", clear);
|
|
290
|
-
document.removeEventListener("touchmove", clear);
|
|
291
|
-
document.removeEventListener("touchcancel", clear);
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
if (gestures.has("shake")) {
|
|
295
|
-
let lx = 0;
|
|
296
|
-
let ly = 0;
|
|
297
|
-
let lz = 0;
|
|
298
|
-
let seeded = false;
|
|
299
|
-
let jolts = 0;
|
|
300
|
-
let windowStart = 0;
|
|
301
|
-
const onMotion = (e) => {
|
|
302
|
-
const a = e.accelerationIncludingGravity;
|
|
303
|
-
if (!a || a.x == null || a.y == null || a.z == null) return;
|
|
304
|
-
if (!seeded) {
|
|
305
|
-
lx = a.x;
|
|
306
|
-
ly = a.y;
|
|
307
|
-
lz = a.z;
|
|
308
|
-
seeded = true;
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
const delta = Math.abs(a.x - lx) + Math.abs(a.y - ly) + Math.abs(a.z - lz);
|
|
312
|
-
lx = a.x;
|
|
313
|
-
ly = a.y;
|
|
314
|
-
lz = a.z;
|
|
315
|
-
if (delta < opts.shakeThreshold) return;
|
|
316
|
-
const now = Date.now();
|
|
317
|
-
if (now - windowStart > 1e3) {
|
|
318
|
-
windowStart = now;
|
|
319
|
-
jolts = 0;
|
|
320
|
-
}
|
|
321
|
-
if (++jolts >= 3) {
|
|
322
|
-
jolts = 0;
|
|
323
|
-
fire();
|
|
324
|
-
}
|
|
325
|
-
};
|
|
326
|
-
window.addEventListener("devicemotion", onMotion);
|
|
327
|
-
cleanups.push(() => window.removeEventListener("devicemotion", onMotion));
|
|
328
|
-
}
|
|
329
|
-
return () => {
|
|
330
|
-
for (const c of cleanups) c();
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
/**
|
|
334
|
-
* React hook: arm the feedback gestures and call `onTrigger` when one fires.
|
|
335
|
-
* Returns the motion-permission state + a primer to request it (iOS).
|
|
336
|
-
*/
|
|
337
|
-
function useFeedbackGesture(onTrigger, options = {}) {
|
|
338
|
-
const cb = useRef(onTrigger);
|
|
339
|
-
cb.current = onTrigger;
|
|
340
|
-
const [motionPermission, setMotionPermission] = useState(() => motionPermissionState());
|
|
341
|
-
const { enabled = true } = options;
|
|
342
|
-
useEffect(() => {
|
|
343
|
-
if (!enabled) return;
|
|
344
|
-
return attachGestureListeners(() => cb.current(), options);
|
|
345
|
-
}, [JSON.stringify({
|
|
346
|
-
g: options.gestures ?? DEFAULT_GESTURES,
|
|
347
|
-
e: options.edgeSize,
|
|
348
|
-
s: options.swipeThreshold,
|
|
349
|
-
d: options.swipeMaxDuration,
|
|
350
|
-
t: options.shakeThreshold,
|
|
351
|
-
p: options.pressDuration,
|
|
352
|
-
c: options.cooldown,
|
|
353
|
-
enabled
|
|
354
|
-
})]);
|
|
355
|
-
return {
|
|
356
|
-
motionPermission,
|
|
357
|
-
requestMotionPermission: async () => {
|
|
358
|
-
const ok = await requestMotionPermission();
|
|
359
|
-
setMotionPermission(ok ? "granted" : "denied");
|
|
360
|
-
return ok;
|
|
361
|
-
}
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
//#endregion
|
|
365
|
-
//#region src/feedback/trace.ts
|
|
366
|
-
let installed = null;
|
|
367
|
-
function push(b) {
|
|
368
|
-
if (!installed) return;
|
|
369
|
-
installed.buffer.push(b);
|
|
370
|
-
if (installed.buffer.length > installed.max) installed.buffer.shift();
|
|
371
|
-
}
|
|
372
|
-
function safeString(v) {
|
|
373
|
-
if (typeof v === "string") return v;
|
|
374
|
-
if (v instanceof Error) return v.message;
|
|
375
|
-
try {
|
|
376
|
-
return JSON.stringify(v);
|
|
377
|
-
} catch {
|
|
378
|
-
return String(v);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
/** Begin collecting breadcrumbs. Idempotent — a second call is a no-op. */
|
|
382
|
-
function installTrace(options = {}) {
|
|
383
|
-
if (installed) return installed.teardown;
|
|
384
|
-
if (typeof window === "undefined") return () => {};
|
|
385
|
-
const max = options.max ?? 50;
|
|
386
|
-
const buffer = [];
|
|
387
|
-
const cleanups = [];
|
|
388
|
-
installed = {
|
|
389
|
-
buffer,
|
|
390
|
-
teardown: () => {},
|
|
391
|
-
max
|
|
392
|
-
};
|
|
393
|
-
if (options.console !== false && typeof console !== "undefined") for (const level of ["error", "warn"]) {
|
|
394
|
-
const orig = console[level];
|
|
395
|
-
if (typeof orig !== "function") continue;
|
|
396
|
-
console[level] = (...args) => {
|
|
397
|
-
push({
|
|
398
|
-
at: Date.now(),
|
|
399
|
-
kind: "console",
|
|
400
|
-
level,
|
|
401
|
-
message: args.map(safeString).join(" ").slice(0, 1e3)
|
|
402
|
-
});
|
|
403
|
-
return orig.apply(console, args);
|
|
404
|
-
};
|
|
405
|
-
cleanups.push(() => {
|
|
406
|
-
console[level] = orig;
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
if (options.errors !== false) {
|
|
410
|
-
const onError = (e) => {
|
|
411
|
-
push({
|
|
412
|
-
at: Date.now(),
|
|
413
|
-
kind: "error",
|
|
414
|
-
message: e.message || "Uncaught error",
|
|
415
|
-
data: {
|
|
416
|
-
source: e.filename,
|
|
417
|
-
line: e.lineno,
|
|
418
|
-
col: e.colno,
|
|
419
|
-
stack: e.error?.stack?.slice(0, 2e3)
|
|
420
|
-
}
|
|
421
|
-
});
|
|
422
|
-
};
|
|
423
|
-
const onRejection = (e) => {
|
|
424
|
-
const r = e.reason;
|
|
425
|
-
push({
|
|
426
|
-
at: Date.now(),
|
|
427
|
-
kind: "rejection",
|
|
428
|
-
message: safeString(r).slice(0, 1e3),
|
|
429
|
-
data: { stack: r?.stack?.slice(0, 2e3) }
|
|
430
|
-
});
|
|
431
|
-
};
|
|
432
|
-
window.addEventListener("error", onError);
|
|
433
|
-
window.addEventListener("unhandledrejection", onRejection);
|
|
434
|
-
cleanups.push(() => {
|
|
435
|
-
window.removeEventListener("error", onError);
|
|
436
|
-
window.removeEventListener("unhandledrejection", onRejection);
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
if (options.network !== false && typeof window.fetch === "function") {
|
|
440
|
-
const origFetch = window.fetch.bind(window);
|
|
441
|
-
window.fetch = async (...args) => {
|
|
442
|
-
const started = Date.now();
|
|
443
|
-
const url = typeof args[0] === "string" ? args[0] : args[0]?.url ?? String(args[0]);
|
|
444
|
-
const method = (args[1]?.method ?? args[0]?.method ?? "GET").toUpperCase();
|
|
445
|
-
try {
|
|
446
|
-
const res = await origFetch(...args);
|
|
447
|
-
const durationMs = Date.now() - started;
|
|
448
|
-
if (!res.ok || durationMs > 2e3) push({
|
|
449
|
-
at: started,
|
|
450
|
-
kind: "network",
|
|
451
|
-
level: method,
|
|
452
|
-
message: `${res.status} ${method} ${url}`.slice(0, 500),
|
|
453
|
-
data: {
|
|
454
|
-
status: res.status,
|
|
455
|
-
durationMs
|
|
456
|
-
}
|
|
457
|
-
});
|
|
458
|
-
return res;
|
|
459
|
-
} catch (err) {
|
|
460
|
-
push({
|
|
461
|
-
at: started,
|
|
462
|
-
kind: "network",
|
|
463
|
-
level: method,
|
|
464
|
-
message: `FAILED ${method} ${url}`.slice(0, 500),
|
|
465
|
-
data: {
|
|
466
|
-
error: safeString(err),
|
|
467
|
-
durationMs: Date.now() - started
|
|
468
|
-
}
|
|
469
|
-
});
|
|
470
|
-
throw err;
|
|
471
|
-
}
|
|
472
|
-
};
|
|
473
|
-
cleanups.push(() => {
|
|
474
|
-
window.fetch = origFetch;
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
const teardown = () => {
|
|
478
|
-
for (const c of cleanups) c();
|
|
479
|
-
installed = null;
|
|
480
|
-
};
|
|
481
|
-
installed.teardown = teardown;
|
|
482
|
-
return teardown;
|
|
483
|
-
}
|
|
484
|
-
/** Snapshot the current breadcrumbs (most-recent last). Empty if not installed. */
|
|
485
|
-
function getTrace() {
|
|
486
|
-
return installed ? [...installed.buffer] : [];
|
|
487
|
-
}
|
|
488
|
-
/** Clear the buffer without uninstalling (e.g. after a report is sent). */
|
|
489
|
-
function clearTrace() {
|
|
490
|
-
if (installed) installed.buffer.length = 0;
|
|
491
|
-
}
|
|
492
|
-
//#endregion
|
|
493
|
-
//#region src/feedback/screenshot.ts
|
|
494
|
-
async function captureScreenshot(options = {}) {
|
|
495
|
-
if (typeof document === "undefined" || typeof window === "undefined") return null;
|
|
496
|
-
const target = options.target ?? document.body;
|
|
497
|
-
if (!target) return null;
|
|
498
|
-
try {
|
|
499
|
-
return await (await import("modern-screenshot")).domToPng(target, {
|
|
500
|
-
scale: options.scale ?? Math.min(window.devicePixelRatio || 1, 2),
|
|
501
|
-
backgroundColor: getComputedStyle(document.body).backgroundColor || "#ffffff",
|
|
502
|
-
filter: (node) => !(node instanceof HTMLElement && node.hasAttribute("data-vibes-feedback"))
|
|
503
|
-
});
|
|
504
|
-
} catch {
|
|
505
|
-
return null;
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
//#endregion
|
|
509
|
-
//#region src/feedback/VibesFeedback.tsx
|
|
510
|
-
const STYLE_ID = "vibes-feedback-styles";
|
|
511
|
-
function ensureStyles() {
|
|
512
|
-
if (typeof document === "undefined" || document.getElementById(STYLE_ID)) return;
|
|
513
|
-
const el = document.createElement("style");
|
|
514
|
-
el.id = STYLE_ID;
|
|
515
|
-
el.textContent = `
|
|
516
|
-
@keyframes vibes-fb-in { from { transform: translateY(100%); } to { transform: translateY(0); } }
|
|
517
|
-
@keyframes vibes-fb-fade { from { opacity: 0; } to { opacity: 1; } }
|
|
518
|
-
.vibes-fb-sheet { animation: vibes-fb-in .26s cubic-bezier(.22,1,.36,1); }
|
|
519
|
-
.vibes-fb-backdrop { animation: vibes-fb-fade .2s ease-out; }
|
|
520
|
-
.vibes-fb-btn:active { transform: scale(.96); }
|
|
521
|
-
`;
|
|
522
|
-
document.head.appendChild(el);
|
|
523
|
-
}
|
|
524
|
-
async function defaultSubmit(report) {
|
|
525
|
-
const res = await fetch("/_report", {
|
|
526
|
-
method: "POST",
|
|
527
|
-
headers: { "content-type": "application/json" },
|
|
528
|
-
body: JSON.stringify(report)
|
|
529
|
-
});
|
|
530
|
-
if (!res.ok) throw new Error(`Feedback failed (${res.status})`);
|
|
531
|
-
}
|
|
532
|
-
function VibesFeedback({ gestures, showButton = true, traceLimit = 50, screenshot = true, captureScreenshot: captureScreenshot$1, onSubmit, accent = "#6366f1", title = "Send feedback" }) {
|
|
533
|
-
const [open, setOpen] = useState(false);
|
|
534
|
-
const [message, setMessage] = useState("");
|
|
535
|
-
const [shot, setShot] = useState(null);
|
|
536
|
-
const [trace, setTrace] = useState([]);
|
|
537
|
-
const [sending, setSending] = useState(false);
|
|
538
|
-
const [done, setDone] = useState(false);
|
|
539
|
-
const [error, setError] = useState(null);
|
|
540
|
-
const { upload } = useUpload();
|
|
541
|
-
const capturer = useMemo(() => captureScreenshot$1 ?? (screenshot ? () => captureScreenshot() : null), [captureScreenshot$1, screenshot]);
|
|
542
|
-
useEffect(() => installTrace({ max: traceLimit }), [traceLimit]);
|
|
543
|
-
const summon = useCallback(() => {
|
|
544
|
-
ensureStyles();
|
|
545
|
-
setTrace(getTrace());
|
|
546
|
-
setMessage("");
|
|
547
|
-
setShot(null);
|
|
548
|
-
setError(null);
|
|
549
|
-
setDone(false);
|
|
550
|
-
setOpen(true);
|
|
551
|
-
if (capturer) capturer().then((s) => setShot(s)).catch(() => setShot(null));
|
|
552
|
-
}, [capturer]);
|
|
553
|
-
const { motionPermission, requestMotionPermission } = useFeedbackGesture(summon, { gestures });
|
|
554
|
-
useEffect(() => {
|
|
555
|
-
if (typeof window === "undefined") return;
|
|
556
|
-
const onOpen = () => summon();
|
|
557
|
-
window.addEventListener("vibes:feedback:open", onOpen);
|
|
558
|
-
return () => window.removeEventListener("vibes:feedback:open", onOpen);
|
|
559
|
-
}, [summon]);
|
|
560
|
-
const shakeEnabled = (gestures ?? DEFAULT_GESTURES).includes("shake");
|
|
561
|
-
const primedRef = useRef(false);
|
|
562
|
-
useEffect(() => {
|
|
563
|
-
if (!shakeEnabled || motionPermission !== "prompt" || primedRef.current) return;
|
|
564
|
-
const prime = () => {
|
|
565
|
-
if (primedRef.current) return;
|
|
566
|
-
primedRef.current = true;
|
|
567
|
-
requestMotionPermission();
|
|
568
|
-
};
|
|
569
|
-
window.addEventListener("pointerdown", prime, {
|
|
570
|
-
once: true,
|
|
571
|
-
capture: true
|
|
572
|
-
});
|
|
573
|
-
return () => window.removeEventListener("pointerdown", prime, { capture: true });
|
|
574
|
-
}, [
|
|
575
|
-
shakeEnabled,
|
|
576
|
-
motionPermission,
|
|
577
|
-
requestMotionPermission
|
|
578
|
-
]);
|
|
579
|
-
async function uploadShot(dataUrl) {
|
|
580
|
-
try {
|
|
581
|
-
const res = await upload(await (await fetch(dataUrl)).blob(), {
|
|
582
|
-
key: `feedback/${Date.now()}-${Math.random().toString(36).slice(2, 8)}.png`,
|
|
583
|
-
scope: "app",
|
|
584
|
-
contentType: "image/png"
|
|
585
|
-
});
|
|
586
|
-
return {
|
|
587
|
-
screenshotKey: res.key,
|
|
588
|
-
screenshotUrl: res.downloadUrl
|
|
589
|
-
};
|
|
590
|
-
} catch {
|
|
591
|
-
return {};
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
async function send() {
|
|
595
|
-
const text = message.trim();
|
|
596
|
-
if (!text || sending) return;
|
|
597
|
-
setSending(true);
|
|
598
|
-
setError(null);
|
|
599
|
-
const shotRefs = shot ? await uploadShot(shot) : {};
|
|
600
|
-
const { user } = getAuthContext();
|
|
601
|
-
const report = {
|
|
602
|
-
message: text,
|
|
603
|
-
...shotRefs,
|
|
604
|
-
trace,
|
|
605
|
-
reporterEmail: user?.email,
|
|
606
|
-
pageUrl: typeof window !== "undefined" ? window.location.href : "",
|
|
607
|
-
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : ""
|
|
608
|
-
};
|
|
609
|
-
try {
|
|
610
|
-
await (onSubmit ?? defaultSubmit)(report);
|
|
611
|
-
clearTrace();
|
|
612
|
-
setDone(true);
|
|
613
|
-
setTimeout(() => setOpen(false), 1100);
|
|
614
|
-
} catch (e) {
|
|
615
|
-
setError(e instanceof Error ? e.message : "Couldn't send. Try again.");
|
|
616
|
-
} finally {
|
|
617
|
-
setSending(false);
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [showButton && /* @__PURE__ */ jsx("button", {
|
|
621
|
-
"aria-label": "Send feedback",
|
|
622
|
-
"data-vibes-feedback": "",
|
|
623
|
-
className: "vibes-fb-btn",
|
|
624
|
-
onClick: () => {
|
|
625
|
-
if (shakeEnabled && motionPermission === "prompt") requestMotionPermission();
|
|
626
|
-
summon();
|
|
627
|
-
},
|
|
628
|
-
style: {
|
|
629
|
-
position: "fixed",
|
|
630
|
-
right: 16,
|
|
631
|
-
bottom: "calc(16px + env(safe-area-inset-bottom))",
|
|
632
|
-
zIndex: 2147482e3,
|
|
633
|
-
width: 44,
|
|
634
|
-
height: 44,
|
|
635
|
-
borderRadius: 999,
|
|
636
|
-
border: "none",
|
|
637
|
-
background: accent,
|
|
638
|
-
color: "#fff",
|
|
639
|
-
display: "grid",
|
|
640
|
-
placeItems: "center",
|
|
641
|
-
boxShadow: "0 4px 16px rgba(0,0,0,.25)",
|
|
642
|
-
cursor: "pointer",
|
|
643
|
-
transition: "transform .12s ease"
|
|
644
|
-
},
|
|
645
|
-
children: /* @__PURE__ */ jsxs("svg", {
|
|
646
|
-
width: "20",
|
|
647
|
-
height: "20",
|
|
648
|
-
viewBox: "0 0 24 24",
|
|
649
|
-
fill: "none",
|
|
650
|
-
stroke: "currentColor",
|
|
651
|
-
strokeWidth: "2",
|
|
652
|
-
strokeLinecap: "round",
|
|
653
|
-
strokeLinejoin: "round",
|
|
654
|
-
children: [/* @__PURE__ */ jsx("path", { d: "m3 11 18-5v12L3 14v-3z" }), /* @__PURE__ */ jsx("path", { d: "M11.6 16.8a3 3 0 1 1-5.8-1.6" })]
|
|
655
|
-
})
|
|
656
|
-
}), open && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
|
|
657
|
-
className: "vibes-fb-backdrop",
|
|
658
|
-
"data-vibes-feedback": "",
|
|
659
|
-
onClick: () => !sending && setOpen(false),
|
|
660
|
-
style: {
|
|
661
|
-
position: "fixed",
|
|
662
|
-
inset: 0,
|
|
663
|
-
zIndex: 2147482900,
|
|
664
|
-
background: "rgba(0,0,0,.4)"
|
|
665
|
-
}
|
|
666
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
667
|
-
className: "vibes-fb-sheet",
|
|
668
|
-
"data-vibes-feedback": "",
|
|
669
|
-
role: "dialog",
|
|
670
|
-
"aria-label": title,
|
|
671
|
-
style: {
|
|
672
|
-
position: "fixed",
|
|
673
|
-
left: 0,
|
|
674
|
-
right: 0,
|
|
675
|
-
bottom: 0,
|
|
676
|
-
zIndex: 2147483e3,
|
|
677
|
-
margin: "0 auto",
|
|
678
|
-
maxWidth: 480,
|
|
679
|
-
background: "#fff",
|
|
680
|
-
color: "#0b0b0f",
|
|
681
|
-
borderTopLeftRadius: 20,
|
|
682
|
-
borderTopRightRadius: 20,
|
|
683
|
-
padding: "16px 16px calc(16px + env(safe-area-inset-bottom))",
|
|
684
|
-
boxShadow: "0 -8px 40px rgba(0,0,0,.22)",
|
|
685
|
-
fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, sans-serif"
|
|
686
|
-
},
|
|
687
|
-
children: [/* @__PURE__ */ jsx("div", { style: {
|
|
688
|
-
width: 36,
|
|
689
|
-
height: 4,
|
|
690
|
-
borderRadius: 999,
|
|
691
|
-
background: "#e2e2e8",
|
|
692
|
-
margin: "0 auto 12px"
|
|
693
|
-
} }), done ? /* @__PURE__ */ jsxs("div", {
|
|
694
|
-
style: {
|
|
695
|
-
textAlign: "center",
|
|
696
|
-
padding: "20px 0 28px"
|
|
697
|
-
},
|
|
698
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
699
|
-
style: {
|
|
700
|
-
fontSize: 32,
|
|
701
|
-
marginBottom: 6
|
|
702
|
-
},
|
|
703
|
-
children: "✓"
|
|
704
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
705
|
-
style: { fontWeight: 600 },
|
|
706
|
-
children: "Thanks — sent."
|
|
707
|
-
})]
|
|
708
|
-
}) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
709
|
-
/* @__PURE__ */ jsx("div", {
|
|
710
|
-
style: {
|
|
711
|
-
fontWeight: 650,
|
|
712
|
-
fontSize: 17,
|
|
713
|
-
marginBottom: 10
|
|
714
|
-
},
|
|
715
|
-
children: title
|
|
716
|
-
}),
|
|
717
|
-
/* @__PURE__ */ jsx("textarea", {
|
|
718
|
-
autoFocus: true,
|
|
719
|
-
value: message,
|
|
720
|
-
onChange: (e) => setMessage(e.target.value),
|
|
721
|
-
placeholder: "What happened?",
|
|
722
|
-
disabled: sending,
|
|
723
|
-
style: {
|
|
724
|
-
width: "100%",
|
|
725
|
-
minHeight: 96,
|
|
726
|
-
resize: "none",
|
|
727
|
-
borderRadius: 12,
|
|
728
|
-
border: "1px solid #e3e3ea",
|
|
729
|
-
padding: 12,
|
|
730
|
-
fontSize: 15,
|
|
731
|
-
fontFamily: "inherit",
|
|
732
|
-
outline: "none",
|
|
733
|
-
boxSizing: "border-box"
|
|
734
|
-
}
|
|
735
|
-
}),
|
|
736
|
-
shot && /* @__PURE__ */ jsx("img", {
|
|
737
|
-
src: shot,
|
|
738
|
-
alt: "screenshot",
|
|
739
|
-
style: {
|
|
740
|
-
marginTop: 10,
|
|
741
|
-
width: "100%",
|
|
742
|
-
maxHeight: 160,
|
|
743
|
-
objectFit: "cover",
|
|
744
|
-
borderRadius: 10,
|
|
745
|
-
border: "1px solid #eee"
|
|
746
|
-
}
|
|
747
|
-
}),
|
|
748
|
-
/* @__PURE__ */ jsxs("div", {
|
|
749
|
-
style: {
|
|
750
|
-
marginTop: 8,
|
|
751
|
-
fontSize: 12,
|
|
752
|
-
color: "#8a8a96"
|
|
753
|
-
},
|
|
754
|
-
children: [trace.length > 0 ? `${trace.length} trace event${trace.length === 1 ? "" : "s"} attached` : "No errors captured", capturer ? shot ? " · screenshot ready" : " · capturing screenshot…" : ""]
|
|
755
|
-
}),
|
|
756
|
-
error && /* @__PURE__ */ jsx("div", {
|
|
757
|
-
style: {
|
|
758
|
-
marginTop: 8,
|
|
759
|
-
fontSize: 13,
|
|
760
|
-
color: "#d4163c"
|
|
761
|
-
},
|
|
762
|
-
children: error
|
|
763
|
-
}),
|
|
764
|
-
/* @__PURE__ */ jsxs("div", {
|
|
765
|
-
style: {
|
|
766
|
-
display: "flex",
|
|
767
|
-
gap: 8,
|
|
768
|
-
marginTop: 14
|
|
769
|
-
},
|
|
770
|
-
children: [/* @__PURE__ */ jsx("button", {
|
|
771
|
-
onClick: () => setOpen(false),
|
|
772
|
-
disabled: sending,
|
|
773
|
-
style: {
|
|
774
|
-
flex: "0 0 auto",
|
|
775
|
-
height: 44,
|
|
776
|
-
padding: "0 16px",
|
|
777
|
-
borderRadius: 12,
|
|
778
|
-
border: "1px solid #e3e3ea",
|
|
779
|
-
background: "#fff",
|
|
780
|
-
color: "#0b0b0f",
|
|
781
|
-
fontSize: 15,
|
|
782
|
-
cursor: "pointer"
|
|
783
|
-
},
|
|
784
|
-
children: "Cancel"
|
|
785
|
-
}), /* @__PURE__ */ jsx("button", {
|
|
786
|
-
className: "vibes-fb-btn",
|
|
787
|
-
onClick: send,
|
|
788
|
-
disabled: !message.trim() || sending,
|
|
789
|
-
style: {
|
|
790
|
-
flex: 1,
|
|
791
|
-
height: 44,
|
|
792
|
-
borderRadius: 12,
|
|
793
|
-
border: "none",
|
|
794
|
-
background: message.trim() && !sending ? accent : "#c7c7d1",
|
|
795
|
-
color: "#fff",
|
|
796
|
-
fontSize: 15,
|
|
797
|
-
fontWeight: 600,
|
|
798
|
-
cursor: message.trim() && !sending ? "pointer" : "default",
|
|
799
|
-
transition: "transform .12s ease, background .12s ease"
|
|
800
|
-
},
|
|
801
|
-
children: sending ? "Sending…" : "Send"
|
|
802
|
-
})]
|
|
803
|
-
})
|
|
804
|
-
] })]
|
|
805
|
-
})] })] });
|
|
806
|
-
}
|
|
807
|
-
//#endregion
|
|
808
|
-
export { installTrace as a, requestMotionPermission as c, getAuthContext as d, notifyAuthRequired as f, subscribeAuthRequired as h, getTrace as i, useFeedbackGesture as l, subscribeAuthChange as m, captureScreenshot as n, attachGestureListeners as o, setAuthContext as p, clearTrace as r, motionPermissionState as s, VibesFeedback as t, useUpload as u };
|