@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.
@@ -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
- }
@@ -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
- }