@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
package/dist/feedback/auto.mjs
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { t as VibesFeedback } from "../VibesFeedback-BF2Vf6FK.mjs";
|
|
2
|
-
import { jsx } from "react/jsx-runtime";
|
|
3
|
-
import { createRoot } from "react-dom/client";
|
|
4
|
-
//#region src/feedback/auto.tsx
|
|
5
|
-
const HOST_ID = "vibes-feedback-auto";
|
|
6
|
-
function isInIframe() {
|
|
7
|
-
try {
|
|
8
|
-
return window.self !== window.top;
|
|
9
|
-
} catch {
|
|
10
|
-
return true;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
function mount() {
|
|
14
|
-
if (typeof window === "undefined" || typeof document === "undefined") return;
|
|
15
|
-
if (isInIframe()) return;
|
|
16
|
-
if (document.getElementById(HOST_ID)) return;
|
|
17
|
-
const host = document.createElement("div");
|
|
18
|
-
host.id = HOST_ID;
|
|
19
|
-
host.setAttribute("data-vibes-feedback", "");
|
|
20
|
-
document.body.appendChild(host);
|
|
21
|
-
createRoot(host).render(/* @__PURE__ */ jsx(VibesFeedback, {}));
|
|
22
|
-
}
|
|
23
|
-
if (typeof window !== "undefined") if (document.readyState === "complete") mount();
|
|
24
|
-
else window.addEventListener("load", mount, { once: true });
|
|
25
|
-
//#endregion
|
|
26
|
-
export {};
|
|
@@ -1,360 +0,0 @@
|
|
|
1
|
-
// @omg-dev/sdk/feedback — gesture-summoned feedback widget.
|
|
2
|
-
//
|
|
3
|
-
// Drop <VibesFeedback /> anywhere in an app and a feedback panel is summoned by
|
|
4
|
-
// a gesture (shake the phone by default, or an edge swipe / two-finger press).
|
|
5
|
-
// It captures the user's message + the recent runtime trace (and a screenshot
|
|
6
|
-
// when a capture fn is supplied) and submits a report.
|
|
7
|
-
//
|
|
8
|
-
// Self-contained styles (inline + one injected <style> for the sheet
|
|
9
|
-
// animation) — it does NOT depend on the host app's Tailwind, so it looks right
|
|
10
|
-
// in any vibes app. Opt-in: the app renders it explicitly (auto-injection via
|
|
11
|
-
// the vite-plugin can come later behind a flag).
|
|
12
|
-
|
|
13
|
-
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from "react"
|
|
14
|
-
import { getAuthContext } from "../auth/bridge"
|
|
15
|
-
import { useUpload } from "../storage/useUpload"
|
|
16
|
-
import { useFeedbackGesture, DEFAULT_GESTURES, type FeedbackGesture } from "./gestures"
|
|
17
|
-
import { installTrace, getTrace, clearTrace, type Breadcrumb } from "./trace"
|
|
18
|
-
import { captureScreenshot as builtinCapture } from "./screenshot"
|
|
19
|
-
|
|
20
|
-
export interface FeedbackReport {
|
|
21
|
-
message: string
|
|
22
|
-
/** Durable storage key of the uploaded screenshot, when captured + uploaded. */
|
|
23
|
-
screenshotKey?: string
|
|
24
|
-
/** Presigned URL for the screenshot (short-lived; for immediate display). */
|
|
25
|
-
screenshotUrl?: string
|
|
26
|
-
/** Recent runtime breadcrumbs (console/error/rejection/network). */
|
|
27
|
-
trace: Breadcrumb[]
|
|
28
|
-
/** Signed-in app user's email, when available (best-effort attribution). */
|
|
29
|
-
reporterEmail?: string
|
|
30
|
-
pageUrl: string
|
|
31
|
-
userAgent: string
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface VibesFeedbackProps {
|
|
35
|
-
/** Gestures that summon the panel. Default: two-finger press (permission-free).
|
|
36
|
-
* "shake" is opt-in — iOS won't persist its motion grant across PWA launches. */
|
|
37
|
-
gestures?: FeedbackGesture[]
|
|
38
|
-
/** Render the always-anchored floating feedback button. Default true — it's the
|
|
39
|
-
* reliable, permission-free way to summon the panel on every platform. */
|
|
40
|
-
showButton?: boolean
|
|
41
|
-
/** Max breadcrumbs to retain + attach. Default 50. */
|
|
42
|
-
traceLimit?: number
|
|
43
|
-
/** Attach a screenshot of the app (lazy-loaded rasterizer). Default true. */
|
|
44
|
-
screenshot?: boolean
|
|
45
|
-
/** Override screenshot capture — returns a data URL (or null). Defaults to the built-in DOM rasterizer. */
|
|
46
|
-
captureScreenshot?: () => Promise<string | null>
|
|
47
|
-
/** Submit handler. When omitted, POSTs JSON to the agent `/_report` route (→ orchestrator → control-plane). */
|
|
48
|
-
onSubmit?: (report: FeedbackReport) => Promise<void>
|
|
49
|
-
/** Accent color. Default indigo. */
|
|
50
|
-
accent?: string
|
|
51
|
-
/** Panel heading. Default "Send feedback". */
|
|
52
|
-
title?: string
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const STYLE_ID = "vibes-feedback-styles"
|
|
56
|
-
function ensureStyles() {
|
|
57
|
-
if (typeof document === "undefined" || document.getElementById(STYLE_ID)) return
|
|
58
|
-
const el = document.createElement("style")
|
|
59
|
-
el.id = STYLE_ID
|
|
60
|
-
el.textContent = `
|
|
61
|
-
@keyframes vibes-fb-in { from { transform: translateY(100%); } to { transform: translateY(0); } }
|
|
62
|
-
@keyframes vibes-fb-fade { from { opacity: 0; } to { opacity: 1; } }
|
|
63
|
-
.vibes-fb-sheet { animation: vibes-fb-in .26s cubic-bezier(.22,1,.36,1); }
|
|
64
|
-
.vibes-fb-backdrop { animation: vibes-fb-fade .2s ease-out; }
|
|
65
|
-
.vibes-fb-btn:active { transform: scale(.96); }
|
|
66
|
-
`
|
|
67
|
-
document.head.appendChild(el)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Default transport: POST the report to the in-VM agent's `/_report` route.
|
|
71
|
-
// The agent forwards it to the orchestrator (svc token + X-On-Behalf-Of: slug),
|
|
72
|
-
// which emits a signed `bug.report.submitted` webhook to the control-plane —
|
|
73
|
-
// so a report from any deployed app lands in the LFG triage panel, no builder
|
|
74
|
-
// JWT required. Works in the dev preview sandbox too (same agent).
|
|
75
|
-
async function defaultSubmit(report: FeedbackReport): Promise<void> {
|
|
76
|
-
const res = await fetch("/_report", {
|
|
77
|
-
method: "POST",
|
|
78
|
-
headers: { "content-type": "application/json" },
|
|
79
|
-
body: JSON.stringify(report),
|
|
80
|
-
})
|
|
81
|
-
if (!res.ok) throw new Error(`Feedback failed (${res.status})`)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function VibesFeedback({
|
|
85
|
-
gestures,
|
|
86
|
-
showButton = true,
|
|
87
|
-
traceLimit = 50,
|
|
88
|
-
screenshot = true,
|
|
89
|
-
captureScreenshot,
|
|
90
|
-
onSubmit,
|
|
91
|
-
accent = "#6366f1",
|
|
92
|
-
title = "Send feedback",
|
|
93
|
-
}: VibesFeedbackProps) {
|
|
94
|
-
const [open, setOpen] = useState(false)
|
|
95
|
-
const [message, setMessage] = useState("")
|
|
96
|
-
const [shot, setShot] = useState<string | null>(null)
|
|
97
|
-
const [trace, setTrace] = useState<Breadcrumb[]>([])
|
|
98
|
-
const [sending, setSending] = useState(false)
|
|
99
|
-
const [done, setDone] = useState(false)
|
|
100
|
-
const [error, setError] = useState<string | null>(null)
|
|
101
|
-
const { upload } = useUpload()
|
|
102
|
-
|
|
103
|
-
// The active capturer: explicit override, else the built-in unless disabled.
|
|
104
|
-
const capturer = useMemo(
|
|
105
|
-
() => captureScreenshot ?? (screenshot ? () => builtinCapture() : null),
|
|
106
|
-
[captureScreenshot, screenshot],
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
// Collect breadcrumbs for the whole app lifetime, not just while open.
|
|
110
|
-
useEffect(() => installTrace({ max: traceLimit }), [traceLimit])
|
|
111
|
-
|
|
112
|
-
const summon = useCallback(() => {
|
|
113
|
-
ensureStyles()
|
|
114
|
-
setTrace(getTrace())
|
|
115
|
-
setMessage("")
|
|
116
|
-
setShot(null)
|
|
117
|
-
setError(null)
|
|
118
|
-
setDone(false)
|
|
119
|
-
setOpen(true)
|
|
120
|
-
// Capture AFTER paint so the screenshot reflects the current view; the
|
|
121
|
-
// panel/backdrop are tagged data-vibes-feedback and filtered out of it.
|
|
122
|
-
if (capturer) {
|
|
123
|
-
capturer()
|
|
124
|
-
.then((s: string | null) => setShot(s))
|
|
125
|
-
.catch(() => setShot(null))
|
|
126
|
-
}
|
|
127
|
-
}, [capturer])
|
|
128
|
-
|
|
129
|
-
const { motionPermission, requestMotionPermission } = useFeedbackGesture(summon, { gestures })
|
|
130
|
-
|
|
131
|
-
// External summon: the brand badge bundles this widget button-less and opens
|
|
132
|
-
// it from its dialog via a window event, so feedback is one control with the
|
|
133
|
-
// omg badge instead of a second floating button.
|
|
134
|
-
useEffect(() => {
|
|
135
|
-
if (typeof window === "undefined") return
|
|
136
|
-
const onOpen = () => summon()
|
|
137
|
-
window.addEventListener("vibes:feedback:open", onOpen)
|
|
138
|
-
return () => window.removeEventListener("vibes:feedback:open", onOpen)
|
|
139
|
-
}, [summon])
|
|
140
|
-
|
|
141
|
-
// Only shake needs the accelerometer; without it we must never trip iOS's
|
|
142
|
-
// motion-permission prompt (it can't be persisted, so priming it just nags
|
|
143
|
-
// the user on every session). Compute against the same default the gesture
|
|
144
|
-
// hook uses so an app that never opts into shake is never prompted.
|
|
145
|
-
const shakeEnabled = (gestures ?? DEFAULT_GESTURES).includes("shake")
|
|
146
|
-
|
|
147
|
-
// iOS chicken-and-egg (shake only): on iOS 13+ the `devicemotion` stream stays
|
|
148
|
-
// silent until permission is granted, and the grant must originate from a user
|
|
149
|
-
// gesture — but the only gesture that would summon our panel is the shake we
|
|
150
|
-
// can't yet detect. Prime it on the first tap anywhere so a later shake fires.
|
|
151
|
-
// No-op elsewhere: off iOS `motionPermission` is "granted"/"unsupported".
|
|
152
|
-
const primedRef = useRef(false)
|
|
153
|
-
useEffect(() => {
|
|
154
|
-
if (!shakeEnabled || motionPermission !== "prompt" || primedRef.current) return
|
|
155
|
-
const prime = () => {
|
|
156
|
-
if (primedRef.current) return
|
|
157
|
-
primedRef.current = true
|
|
158
|
-
void requestMotionPermission()
|
|
159
|
-
}
|
|
160
|
-
window.addEventListener("pointerdown", prime, { once: true, capture: true })
|
|
161
|
-
return () => window.removeEventListener("pointerdown", prime, { capture: true })
|
|
162
|
-
}, [shakeEnabled, motionPermission, requestMotionPermission])
|
|
163
|
-
|
|
164
|
-
// Upload the screenshot to app storage (Tigris-backed via the agent presign)
|
|
165
|
-
// and return a durable key + short-lived URL. Best-effort — a failure just
|
|
166
|
-
// drops the screenshot, the rest of the report still sends.
|
|
167
|
-
async function uploadShot(dataUrl: string): Promise<{ screenshotKey?: string; screenshotUrl?: string }> {
|
|
168
|
-
try {
|
|
169
|
-
const blob = await (await fetch(dataUrl)).blob()
|
|
170
|
-
const key = `feedback/${Date.now()}-${Math.random().toString(36).slice(2, 8)}.png`
|
|
171
|
-
const res = await upload(blob, { key, scope: "app", contentType: "image/png" })
|
|
172
|
-
return { screenshotKey: res.key, screenshotUrl: res.downloadUrl }
|
|
173
|
-
} catch {
|
|
174
|
-
return {}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
async function send() {
|
|
179
|
-
const text = message.trim()
|
|
180
|
-
if (!text || sending) return
|
|
181
|
-
setSending(true)
|
|
182
|
-
setError(null)
|
|
183
|
-
const shotRefs = shot ? await uploadShot(shot) : {}
|
|
184
|
-
const { user } = getAuthContext()
|
|
185
|
-
const report: FeedbackReport = {
|
|
186
|
-
message: text,
|
|
187
|
-
...shotRefs,
|
|
188
|
-
trace,
|
|
189
|
-
reporterEmail: (user as { email?: string } | null)?.email,
|
|
190
|
-
pageUrl: typeof window !== "undefined" ? window.location.href : "",
|
|
191
|
-
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : "",
|
|
192
|
-
}
|
|
193
|
-
try {
|
|
194
|
-
await (onSubmit ?? defaultSubmit)(report)
|
|
195
|
-
clearTrace()
|
|
196
|
-
setDone(true)
|
|
197
|
-
setTimeout(() => setOpen(false), 1100)
|
|
198
|
-
} catch (e) {
|
|
199
|
-
setError(e instanceof Error ? e.message : "Couldn't send. Try again.")
|
|
200
|
-
} finally {
|
|
201
|
-
setSending(false)
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const sheet: CSSProperties = {
|
|
206
|
-
position: "fixed",
|
|
207
|
-
left: 0,
|
|
208
|
-
right: 0,
|
|
209
|
-
bottom: 0,
|
|
210
|
-
zIndex: 2147483000,
|
|
211
|
-
margin: "0 auto",
|
|
212
|
-
maxWidth: 480,
|
|
213
|
-
background: "#fff",
|
|
214
|
-
color: "#0b0b0f",
|
|
215
|
-
borderTopLeftRadius: 20,
|
|
216
|
-
borderTopRightRadius: 20,
|
|
217
|
-
padding: "16px 16px calc(16px + env(safe-area-inset-bottom))",
|
|
218
|
-
boxShadow: "0 -8px 40px rgba(0,0,0,.22)",
|
|
219
|
-
fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, sans-serif",
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return (
|
|
223
|
-
<>
|
|
224
|
-
{showButton && (
|
|
225
|
-
<button
|
|
226
|
-
aria-label="Send feedback"
|
|
227
|
-
data-vibes-feedback=""
|
|
228
|
-
className="vibes-fb-btn"
|
|
229
|
-
onClick={() => {
|
|
230
|
-
// Tapping the button just summons the panel — it never needs motion.
|
|
231
|
-
// Only ask for the accelerometer here when shake is actually armed,
|
|
232
|
-
// so a tap can't surface an unsolicited iOS permission prompt.
|
|
233
|
-
if (shakeEnabled && motionPermission === "prompt") void requestMotionPermission()
|
|
234
|
-
summon()
|
|
235
|
-
}}
|
|
236
|
-
style={{
|
|
237
|
-
position: "fixed",
|
|
238
|
-
right: 16,
|
|
239
|
-
bottom: "calc(16px + env(safe-area-inset-bottom))",
|
|
240
|
-
zIndex: 2147482000,
|
|
241
|
-
width: 44,
|
|
242
|
-
height: 44,
|
|
243
|
-
borderRadius: 999,
|
|
244
|
-
border: "none",
|
|
245
|
-
background: accent,
|
|
246
|
-
color: "#fff",
|
|
247
|
-
display: "grid",
|
|
248
|
-
placeItems: "center",
|
|
249
|
-
boxShadow: "0 4px 16px rgba(0,0,0,.25)",
|
|
250
|
-
cursor: "pointer",
|
|
251
|
-
transition: "transform .12s ease",
|
|
252
|
-
}}
|
|
253
|
-
>
|
|
254
|
-
{/* megaphone glyph */}
|
|
255
|
-
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
256
|
-
<path d="m3 11 18-5v12L3 14v-3z" />
|
|
257
|
-
<path d="M11.6 16.8a3 3 0 1 1-5.8-1.6" />
|
|
258
|
-
</svg>
|
|
259
|
-
</button>
|
|
260
|
-
)}
|
|
261
|
-
|
|
262
|
-
{open && (
|
|
263
|
-
<>
|
|
264
|
-
<div
|
|
265
|
-
className="vibes-fb-backdrop"
|
|
266
|
-
data-vibes-feedback=""
|
|
267
|
-
onClick={() => !sending && setOpen(false)}
|
|
268
|
-
style={{ position: "fixed", inset: 0, zIndex: 2147482900, background: "rgba(0,0,0,.4)" }}
|
|
269
|
-
/>
|
|
270
|
-
<div className="vibes-fb-sheet" data-vibes-feedback="" role="dialog" aria-label={title} style={sheet}>
|
|
271
|
-
<div style={{ width: 36, height: 4, borderRadius: 999, background: "#e2e2e8", margin: "0 auto 12px" }} />
|
|
272
|
-
{done ? (
|
|
273
|
-
<div style={{ textAlign: "center", padding: "20px 0 28px" }}>
|
|
274
|
-
<div style={{ fontSize: 32, marginBottom: 6 }}>✓</div>
|
|
275
|
-
<div style={{ fontWeight: 600 }}>Thanks — sent.</div>
|
|
276
|
-
</div>
|
|
277
|
-
) : (
|
|
278
|
-
<>
|
|
279
|
-
<div style={{ fontWeight: 650, fontSize: 17, marginBottom: 10 }}>{title}</div>
|
|
280
|
-
<textarea
|
|
281
|
-
autoFocus
|
|
282
|
-
value={message}
|
|
283
|
-
onChange={(e) => setMessage(e.target.value)}
|
|
284
|
-
placeholder="What happened?"
|
|
285
|
-
disabled={sending}
|
|
286
|
-
style={{
|
|
287
|
-
width: "100%",
|
|
288
|
-
minHeight: 96,
|
|
289
|
-
resize: "none",
|
|
290
|
-
borderRadius: 12,
|
|
291
|
-
border: "1px solid #e3e3ea",
|
|
292
|
-
padding: 12,
|
|
293
|
-
fontSize: 15,
|
|
294
|
-
fontFamily: "inherit",
|
|
295
|
-
outline: "none",
|
|
296
|
-
boxSizing: "border-box",
|
|
297
|
-
}}
|
|
298
|
-
/>
|
|
299
|
-
|
|
300
|
-
{shot && (
|
|
301
|
-
<img
|
|
302
|
-
src={shot}
|
|
303
|
-
alt="screenshot"
|
|
304
|
-
style={{ marginTop: 10, width: "100%", maxHeight: 160, objectFit: "cover", borderRadius: 10, border: "1px solid #eee" }}
|
|
305
|
-
/>
|
|
306
|
-
)}
|
|
307
|
-
|
|
308
|
-
<div style={{ marginTop: 8, fontSize: 12, color: "#8a8a96" }}>
|
|
309
|
-
{trace.length > 0 ? `${trace.length} trace event${trace.length === 1 ? "" : "s"} attached` : "No errors captured"}
|
|
310
|
-
{capturer ? (shot ? " · screenshot ready" : " · capturing screenshot…") : ""}
|
|
311
|
-
</div>
|
|
312
|
-
|
|
313
|
-
{error && <div style={{ marginTop: 8, fontSize: 13, color: "#d4163c" }}>{error}</div>}
|
|
314
|
-
|
|
315
|
-
<div style={{ display: "flex", gap: 8, marginTop: 14 }}>
|
|
316
|
-
<button
|
|
317
|
-
onClick={() => setOpen(false)}
|
|
318
|
-
disabled={sending}
|
|
319
|
-
style={{
|
|
320
|
-
flex: "0 0 auto",
|
|
321
|
-
height: 44,
|
|
322
|
-
padding: "0 16px",
|
|
323
|
-
borderRadius: 12,
|
|
324
|
-
border: "1px solid #e3e3ea",
|
|
325
|
-
background: "#fff",
|
|
326
|
-
color: "#0b0b0f",
|
|
327
|
-
fontSize: 15,
|
|
328
|
-
cursor: "pointer",
|
|
329
|
-
}}
|
|
330
|
-
>
|
|
331
|
-
Cancel
|
|
332
|
-
</button>
|
|
333
|
-
<button
|
|
334
|
-
className="vibes-fb-btn"
|
|
335
|
-
onClick={send}
|
|
336
|
-
disabled={!message.trim() || sending}
|
|
337
|
-
style={{
|
|
338
|
-
flex: 1,
|
|
339
|
-
height: 44,
|
|
340
|
-
borderRadius: 12,
|
|
341
|
-
border: "none",
|
|
342
|
-
background: message.trim() && !sending ? accent : "#c7c7d1",
|
|
343
|
-
color: "#fff",
|
|
344
|
-
fontSize: 15,
|
|
345
|
-
fontWeight: 600,
|
|
346
|
-
cursor: message.trim() && !sending ? "pointer" : "default",
|
|
347
|
-
transition: "transform .12s ease, background .12s ease",
|
|
348
|
-
}}
|
|
349
|
-
>
|
|
350
|
-
{sending ? "Sending…" : "Send"}
|
|
351
|
-
</button>
|
|
352
|
-
</div>
|
|
353
|
-
</>
|
|
354
|
-
)}
|
|
355
|
-
</div>
|
|
356
|
-
</>
|
|
357
|
-
)}
|
|
358
|
-
</>
|
|
359
|
-
)
|
|
360
|
-
}
|
package/src/feedback/auto.tsx
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
// @omg-dev/sdk/feedback/auto — report-a-bug widget, injected into published
|
|
2
|
-
// builds by @omg-dev/vite-plugin (appended import in the app entry; see index.ts).
|
|
3
|
-
//
|
|
4
|
-
// Mounts <VibesFeedback/> OUTSIDE the app's React tree (own root, own DOM
|
|
5
|
-
// node) so it survives whatever the build agent does to App.tsx/main.tsx.
|
|
6
|
-
// An always-anchored floating button summons the feedback sheet (a two-finger
|
|
7
|
-
// press also works; shake is opt-in — iOS won't persist its motion grant). The
|
|
8
|
-
// report lands in the builder's LFG triage panel via the agent /_report route.
|
|
9
|
-
//
|
|
10
|
-
// Never mounts in the dashboard preview iframe — a shake there is meaningless
|
|
11
|
-
// and the report would have no published slug to attribute to. Apps that want
|
|
12
|
-
// manual placement opt out via vibes({ feedback: false }) and render
|
|
13
|
-
// <VibesFeedback/> from @omg-dev/sdk/feedback themselves.
|
|
14
|
-
|
|
15
|
-
import { createRoot } from "react-dom/client"
|
|
16
|
-
import { VibesFeedback } from "./VibesFeedback"
|
|
17
|
-
|
|
18
|
-
const HOST_ID = "vibes-feedback-auto"
|
|
19
|
-
|
|
20
|
-
function isInIframe(): boolean {
|
|
21
|
-
try {
|
|
22
|
-
return window.self !== window.top
|
|
23
|
-
} catch {
|
|
24
|
-
// Cross-origin access throws — we're framed.
|
|
25
|
-
return true
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function mount() {
|
|
30
|
-
if (typeof window === "undefined" || typeof document === "undefined") return
|
|
31
|
-
if (isInIframe()) return
|
|
32
|
-
if (document.getElementById(HOST_ID)) return
|
|
33
|
-
const host = document.createElement("div")
|
|
34
|
-
host.id = HOST_ID
|
|
35
|
-
// Exclude the whole widget subtree from its own screenshot rasterizer.
|
|
36
|
-
host.setAttribute("data-vibes-feedback", "")
|
|
37
|
-
document.body.appendChild(host)
|
|
38
|
-
createRoot(host).render(<VibesFeedback />)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (typeof window !== "undefined") {
|
|
42
|
-
if (document.readyState === "complete") {
|
|
43
|
-
mount()
|
|
44
|
-
} else {
|
|
45
|
-
window.addEventListener("load", mount, { once: true })
|
|
46
|
-
}
|
|
47
|
-
}
|