@omg-dev/sdk 0.4.24
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-LAcimQp0.mjs +345 -0
- package/dist/VibesFeedback-BF2Vf6FK.mjs +808 -0
- package/dist/brand/auto.mjs +18 -0
- package/dist/feedback/auto.mjs +26 -0
- package/dist/index.mjs +1611 -0
- package/package.json +43 -0
- package/src/auth/auto-prompt.tsx +50 -0
- package/src/auth/bridge.ts +63 -0
- package/src/auth/client.ts +222 -0
- package/src/auth/fetch.ts +23 -0
- package/src/auth/guard.tsx +24 -0
- package/src/auth/index.ts +23 -0
- package/src/auth/login.tsx +267 -0
- package/src/auth/mail-apps.ts +52 -0
- package/src/auth/react.tsx +248 -0
- package/src/brand/OmgBadge.tsx +366 -0
- package/src/brand/auto.tsx +35 -0
- package/src/brand/index.ts +1 -0
- package/src/feedback/VibesFeedback.tsx +360 -0
- package/src/feedback/auto.tsx +47 -0
- package/src/feedback/gestures.ts +296 -0
- package/src/feedback/index.ts +18 -0
- package/src/feedback/screenshot.ts +37 -0
- package/src/feedback/trace.ts +166 -0
- package/src/index.ts +1042 -0
- package/src/notifications/index.tsx +179 -0
- package/src/sandbox.test.ts +61 -0
- package/src/sandbox.ts +106 -0
- package/src/storage/VibesUpload.tsx +140 -0
- package/src/storage/index.ts +12 -0
- package/src/storage/useUpload.ts +167 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { useId, useState, type CSSProperties } from "react"
|
|
2
|
+
import { VibesFeedback } from "../feedback/VibesFeedback"
|
|
3
|
+
|
|
4
|
+
export interface OmgBadgeProps {
|
|
5
|
+
/** Link used by the dialog CTA. Default: https://omg.dev */
|
|
6
|
+
href?: string
|
|
7
|
+
/** Accessible label for the floating badge button. */
|
|
8
|
+
label?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// The one brand colour. Don't recolour it — the coral IS the brand.
|
|
12
|
+
const BRAND = "#FF5530"
|
|
13
|
+
const Z_BADGE = 2147482000
|
|
14
|
+
const Z_DIALOG = 2147483100
|
|
15
|
+
|
|
16
|
+
// Cross-process signal the brand badge dispatches to summon the (button-less)
|
|
17
|
+
// feedback sheet it mounts. Bundling feedback INTO this badge is deliberate:
|
|
18
|
+
// one floating control in the corner, not three.
|
|
19
|
+
export const FEEDBACK_OPEN_EVENT = "vibes:feedback:open"
|
|
20
|
+
|
|
21
|
+
// Build-time gate (set by @omg-dev/vite-plugin via `define`). True when the app
|
|
22
|
+
// has feedback enabled AND the brand badge owns it. Falls back to true when the
|
|
23
|
+
// badge is used outside the plugin (it's only auto-injected by the plugin, so
|
|
24
|
+
// in practice the define is always present in published bundles).
|
|
25
|
+
declare const __VIBES_BRAND_FEEDBACK__: boolean | undefined
|
|
26
|
+
function feedbackBundled(): boolean {
|
|
27
|
+
try {
|
|
28
|
+
return typeof __VIBES_BRAND_FEEDBACK__ === "undefined" ? true : __VIBES_BRAND_FEEDBACK__
|
|
29
|
+
} catch {
|
|
30
|
+
return true
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── Remix ("Make it mine") gating ────────────────────────────────────────────
|
|
35
|
+
// The badge's first job on a PUBLISHED app is the recipient→creator hinge: a
|
|
36
|
+
// visitor who only has the shared link can fork the app back on omg.dev. Once
|
|
37
|
+
// they dismiss that, the SAME badge collapses to plain branding — the omg mark
|
|
38
|
+
// never leaves, only the call-to-action does.
|
|
39
|
+
const DISMISS_KEY = "vibes-remix:dismissed"
|
|
40
|
+
const REMIX_ORIGIN =
|
|
41
|
+
(typeof import.meta !== "undefined" &&
|
|
42
|
+
(import.meta as { env?: Record<string, string | undefined> }).env?.VITE_REMIX_ORIGIN) ||
|
|
43
|
+
"https://omg.dev"
|
|
44
|
+
|
|
45
|
+
function appSlug(): string | null {
|
|
46
|
+
if (typeof import.meta === "undefined") return null
|
|
47
|
+
const env = (import.meta as { env?: Record<string, string | undefined> }).env
|
|
48
|
+
const slug = env?.VITE_APP_SLUG?.trim()
|
|
49
|
+
if (!slug || !/^[a-z0-9-]+$/.test(slug)) return null
|
|
50
|
+
return slug
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isDismissed(): boolean {
|
|
54
|
+
try {
|
|
55
|
+
return localStorage.getItem(DISMISS_KEY) === "1"
|
|
56
|
+
} catch {
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function setDismissedFlag() {
|
|
62
|
+
try {
|
|
63
|
+
localStorage.setItem(DISMISS_KEY, "1")
|
|
64
|
+
} catch {
|
|
65
|
+
/* private mode */
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function OmgBadge({
|
|
70
|
+
href = "https://omg.dev",
|
|
71
|
+
label = "What is omg?",
|
|
72
|
+
}: OmgBadgeProps) {
|
|
73
|
+
const [open, setOpen] = useState(false)
|
|
74
|
+
// "Make it mine" shows only on a published app (slug baked in) that the
|
|
75
|
+
// visitor hasn't dismissed yet. Dismissing keeps the badge — it just drops to
|
|
76
|
+
// the branding state — so the omg mark stays put.
|
|
77
|
+
const slug = appSlug()
|
|
78
|
+
const [remix, setRemix] = useState(() => slug !== null && !isDismissed())
|
|
79
|
+
const titleId = useId()
|
|
80
|
+
const descriptionId = useId()
|
|
81
|
+
const markId = useId().replace(/[^a-zA-Z0-9_-]/g, "")
|
|
82
|
+
|
|
83
|
+
function dismissRemix() {
|
|
84
|
+
setDismissedFlag()
|
|
85
|
+
setRemix(false)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function summonFeedback() {
|
|
89
|
+
setOpen(false)
|
|
90
|
+
if (typeof window !== "undefined") {
|
|
91
|
+
window.dispatchEvent(new CustomEvent(FEEDBACK_OPEN_EVENT))
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<>
|
|
97
|
+
{remix && slug ? (
|
|
98
|
+
// Remix state: the omg mark + "Make it mine" CTA. Tapping the pill body
|
|
99
|
+
// navigates to the dashboard to fork the app; the ✕ collapses to the
|
|
100
|
+
// branding badge. ?ref=pwa attributes the click on /remix.
|
|
101
|
+
<a
|
|
102
|
+
href={`${REMIX_ORIGIN}/remix/${slug}?ref=pwa`}
|
|
103
|
+
aria-label="Make this app mine"
|
|
104
|
+
data-vibes-feedback=""
|
|
105
|
+
style={badgeStyle}
|
|
106
|
+
>
|
|
107
|
+
<OmgMark size={18} id={`remix-${markId}`} />
|
|
108
|
+
<span style={badgeTextStyle}>Make it mine</span>
|
|
109
|
+
<button
|
|
110
|
+
type="button"
|
|
111
|
+
aria-label="Dismiss"
|
|
112
|
+
onClick={(event) => {
|
|
113
|
+
event.preventDefault()
|
|
114
|
+
event.stopPropagation()
|
|
115
|
+
dismissRemix()
|
|
116
|
+
}}
|
|
117
|
+
style={dismissStyle}
|
|
118
|
+
>
|
|
119
|
+
<svg aria-hidden="true" width="13" height="13" viewBox="0 0 24 24" fill="none">
|
|
120
|
+
<path d="M6 6l12 12M18 6 6 18" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" />
|
|
121
|
+
</svg>
|
|
122
|
+
</button>
|
|
123
|
+
</a>
|
|
124
|
+
) : (
|
|
125
|
+
// Branding state: the omg mark + "omg". Opens the "Built with omg"
|
|
126
|
+
// dialog, which also hosts the feedback entry.
|
|
127
|
+
<button
|
|
128
|
+
type="button"
|
|
129
|
+
aria-label={label}
|
|
130
|
+
data-vibes-feedback=""
|
|
131
|
+
onClick={() => setOpen(true)}
|
|
132
|
+
style={badgeStyle}
|
|
133
|
+
>
|
|
134
|
+
<OmgMark size={18} id={`badge-${markId}`} />
|
|
135
|
+
<span style={badgeTextStyle}>omg</span>
|
|
136
|
+
</button>
|
|
137
|
+
)}
|
|
138
|
+
|
|
139
|
+
{open && (
|
|
140
|
+
<div
|
|
141
|
+
data-vibes-feedback=""
|
|
142
|
+
role="presentation"
|
|
143
|
+
style={backdropStyle}
|
|
144
|
+
onMouseDown={(event) => {
|
|
145
|
+
if (event.target === event.currentTarget) setOpen(false)
|
|
146
|
+
}}
|
|
147
|
+
>
|
|
148
|
+
<div
|
|
149
|
+
role="dialog"
|
|
150
|
+
aria-modal="true"
|
|
151
|
+
aria-labelledby={titleId}
|
|
152
|
+
aria-describedby={descriptionId}
|
|
153
|
+
style={dialogStyle}
|
|
154
|
+
>
|
|
155
|
+
<button
|
|
156
|
+
type="button"
|
|
157
|
+
aria-label="Close"
|
|
158
|
+
onClick={() => setOpen(false)}
|
|
159
|
+
style={closeButtonStyle}
|
|
160
|
+
>
|
|
161
|
+
<svg aria-hidden="true" width="18" height="18" viewBox="0 0 24 24" fill="none">
|
|
162
|
+
<path d="M6 6l12 12M18 6 6 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
|
163
|
+
</svg>
|
|
164
|
+
</button>
|
|
165
|
+
|
|
166
|
+
<div style={markWrapStyle}>
|
|
167
|
+
<OmgMark size={34} id={`dialog-${markId}`} />
|
|
168
|
+
</div>
|
|
169
|
+
<h2 id={titleId} style={titleStyle}>
|
|
170
|
+
Built with omg
|
|
171
|
+
</h2>
|
|
172
|
+
<p id={descriptionId} style={bodyStyle}>
|
|
173
|
+
omg turns a plain-language idea into a real app, then hosts it with sign-in,
|
|
174
|
+
data storage, and publishing already handled.
|
|
175
|
+
</p>
|
|
176
|
+
<a href={href} target="_blank" rel="noopener noreferrer" style={ctaStyle}>
|
|
177
|
+
Learn about omg
|
|
178
|
+
</a>
|
|
179
|
+
{feedbackBundled() && (
|
|
180
|
+
<button type="button" onClick={summonFeedback} style={secondaryStyle}>
|
|
181
|
+
Send feedback
|
|
182
|
+
</button>
|
|
183
|
+
)}
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
|
+
|
|
188
|
+
{/* Feedback is bundled into the badge — a button-less sheet summoned from
|
|
189
|
+
the dialog (or by gesture), so there's no second floating control. */}
|
|
190
|
+
{feedbackBundled() && (
|
|
191
|
+
<VibesFeedback showButton={false} accent={BRAND} title="Send feedback" />
|
|
192
|
+
)}
|
|
193
|
+
</>
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// The Gasp — a filled coral disc with a single circular bite at the top-right
|
|
198
|
+
// (the startled "o" mouth). Matches the canonical mark in apps/web; the bite
|
|
199
|
+
// radius nudges up at small sizes so the gap survives anti-aliasing. The old
|
|
200
|
+
// badge mark cut a centred hole + a side dot, which was NOT the brand mark.
|
|
201
|
+
function OmgMark({ size, id }: { size: number; id: string }) {
|
|
202
|
+
const maskId = `omg-mark-cut-${id}`
|
|
203
|
+
const biteR = size <= 16 ? 16.5 : size <= 20 ? 15.2 : size <= 32 ? 14 : 13.2
|
|
204
|
+
return (
|
|
205
|
+
<svg
|
|
206
|
+
aria-hidden="true"
|
|
207
|
+
width={size}
|
|
208
|
+
height={size}
|
|
209
|
+
viewBox="0 0 100 100"
|
|
210
|
+
fill="none"
|
|
211
|
+
style={{ display: "block", flex: "0 0 auto" }}
|
|
212
|
+
>
|
|
213
|
+
<mask id={maskId}>
|
|
214
|
+
<rect width="100" height="100" fill="white" />
|
|
215
|
+
<circle cx="71" cy="29" r={biteR} fill="black" />
|
|
216
|
+
</mask>
|
|
217
|
+
<circle cx="50" cy="50" r="44" fill={BRAND} mask={`url(#${maskId})`} />
|
|
218
|
+
</svg>
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const font = "system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif"
|
|
223
|
+
|
|
224
|
+
const badgeStyle: CSSProperties = {
|
|
225
|
+
position: "fixed",
|
|
226
|
+
right: "max(14px, env(safe-area-inset-right))",
|
|
227
|
+
bottom: "calc(16px + env(safe-area-inset-bottom))",
|
|
228
|
+
zIndex: Z_BADGE,
|
|
229
|
+
height: 38,
|
|
230
|
+
padding: "0 8px 0 10px",
|
|
231
|
+
border: "1px solid rgba(20, 16, 12, 0.12)",
|
|
232
|
+
borderRadius: 999,
|
|
233
|
+
background: "rgba(255, 255, 255, 0.94)",
|
|
234
|
+
color: "#15110d",
|
|
235
|
+
boxShadow: "0 8px 28px rgba(0, 0, 0, 0.16)",
|
|
236
|
+
display: "inline-flex",
|
|
237
|
+
alignItems: "center",
|
|
238
|
+
gap: 7,
|
|
239
|
+
fontFamily: font,
|
|
240
|
+
cursor: "pointer",
|
|
241
|
+
textDecoration: "none",
|
|
242
|
+
WebkitTapHighlightColor: "transparent",
|
|
243
|
+
backdropFilter: "blur(10px)",
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const badgeTextStyle: CSSProperties = {
|
|
247
|
+
fontSize: 13,
|
|
248
|
+
lineHeight: "18px",
|
|
249
|
+
fontWeight: 700,
|
|
250
|
+
letterSpacing: 0,
|
|
251
|
+
padding: "0 3px",
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const dismissStyle: CSSProperties = {
|
|
255
|
+
width: 22,
|
|
256
|
+
height: 22,
|
|
257
|
+
flex: "0 0 auto",
|
|
258
|
+
borderRadius: 999,
|
|
259
|
+
border: "none",
|
|
260
|
+
background: "rgba(20, 16, 12, 0.06)",
|
|
261
|
+
color: "#6b6459",
|
|
262
|
+
display: "grid",
|
|
263
|
+
placeItems: "center",
|
|
264
|
+
cursor: "pointer",
|
|
265
|
+
padding: 0,
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const backdropStyle: CSSProperties = {
|
|
269
|
+
position: "fixed",
|
|
270
|
+
inset: 0,
|
|
271
|
+
zIndex: Z_DIALOG,
|
|
272
|
+
display: "grid",
|
|
273
|
+
placeItems: "center",
|
|
274
|
+
padding: "24px 16px calc(24px + env(safe-area-inset-bottom))",
|
|
275
|
+
background: "rgba(15, 13, 11, 0.34)",
|
|
276
|
+
boxSizing: "border-box",
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const dialogStyle: CSSProperties = {
|
|
280
|
+
position: "relative",
|
|
281
|
+
width: "min(100%, 360px)",
|
|
282
|
+
borderRadius: 18,
|
|
283
|
+
padding: "26px 22px 20px",
|
|
284
|
+
background: "#fffaf4",
|
|
285
|
+
color: "#15110d",
|
|
286
|
+
boxShadow: "0 24px 70px rgba(0, 0, 0, 0.28)",
|
|
287
|
+
border: "1px solid rgba(21, 17, 13, 0.08)",
|
|
288
|
+
fontFamily: font,
|
|
289
|
+
boxSizing: "border-box",
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const closeButtonStyle: CSSProperties = {
|
|
293
|
+
position: "absolute",
|
|
294
|
+
top: 10,
|
|
295
|
+
right: 10,
|
|
296
|
+
width: 34,
|
|
297
|
+
height: 34,
|
|
298
|
+
borderRadius: 999,
|
|
299
|
+
border: "none",
|
|
300
|
+
background: "transparent",
|
|
301
|
+
color: "#6b6459",
|
|
302
|
+
display: "grid",
|
|
303
|
+
placeItems: "center",
|
|
304
|
+
cursor: "pointer",
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const markWrapStyle: CSSProperties = {
|
|
308
|
+
width: 52,
|
|
309
|
+
height: 52,
|
|
310
|
+
borderRadius: 16,
|
|
311
|
+
display: "grid",
|
|
312
|
+
placeItems: "center",
|
|
313
|
+
background: "#fff",
|
|
314
|
+
boxShadow: "inset 0 0 0 1px rgba(21, 17, 13, 0.08)",
|
|
315
|
+
marginBottom: 16,
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const titleStyle: CSSProperties = {
|
|
319
|
+
margin: 0,
|
|
320
|
+
color: "#15110d",
|
|
321
|
+
fontSize: 23,
|
|
322
|
+
lineHeight: "28px",
|
|
323
|
+
fontWeight: 750,
|
|
324
|
+
letterSpacing: 0,
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const bodyStyle: CSSProperties = {
|
|
328
|
+
margin: "10px 0 18px",
|
|
329
|
+
color: "#5f574d",
|
|
330
|
+
fontSize: 15,
|
|
331
|
+
lineHeight: "22px",
|
|
332
|
+
letterSpacing: 0,
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const ctaStyle: CSSProperties = {
|
|
336
|
+
display: "inline-flex",
|
|
337
|
+
alignItems: "center",
|
|
338
|
+
justifyContent: "center",
|
|
339
|
+
width: "100%",
|
|
340
|
+
height: 44,
|
|
341
|
+
borderRadius: 12,
|
|
342
|
+
background: "#15110d",
|
|
343
|
+
color: "#fffaf4",
|
|
344
|
+
fontSize: 15,
|
|
345
|
+
fontWeight: 700,
|
|
346
|
+
textDecoration: "none",
|
|
347
|
+
letterSpacing: 0,
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const secondaryStyle: CSSProperties = {
|
|
351
|
+
display: "inline-flex",
|
|
352
|
+
alignItems: "center",
|
|
353
|
+
justifyContent: "center",
|
|
354
|
+
width: "100%",
|
|
355
|
+
height: 44,
|
|
356
|
+
marginTop: 8,
|
|
357
|
+
borderRadius: 12,
|
|
358
|
+
background: "transparent",
|
|
359
|
+
border: "1px solid rgba(21, 17, 13, 0.14)",
|
|
360
|
+
color: "#15110d",
|
|
361
|
+
fontSize: 15,
|
|
362
|
+
fontWeight: 650,
|
|
363
|
+
cursor: "pointer",
|
|
364
|
+
fontFamily: font,
|
|
365
|
+
letterSpacing: 0,
|
|
366
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// @omg-dev/sdk/brand/auto — the single omg badge for published apps.
|
|
2
|
+
//
|
|
3
|
+
// Injected into published builds by @omg-dev/vite-plugin. It mounts outside the
|
|
4
|
+
// app's React tree so it survives user app rewrites and does not require each
|
|
5
|
+
// generated app to carry attribution code in its source.
|
|
6
|
+
//
|
|
7
|
+
// One control, not three: this badge is the "Make it mine" remix CTA on a fresh
|
|
8
|
+
// visit (same omg mark), collapses to the plain branding badge on dismiss, and
|
|
9
|
+
// bundles the (button-less) feedback sheet, summoned from its dialog. The
|
|
10
|
+
// vite-plugin therefore skips the standalone feedback/auto + remix-cta imports
|
|
11
|
+
// whenever this badge is enabled (the default).
|
|
12
|
+
|
|
13
|
+
import { createRoot } from "react-dom/client"
|
|
14
|
+
import { OmgBadge } from "./OmgBadge"
|
|
15
|
+
|
|
16
|
+
const HOST_ID = "vibes-brand-badge-auto"
|
|
17
|
+
|
|
18
|
+
function mount() {
|
|
19
|
+
if (typeof window === "undefined" || typeof document === "undefined") return
|
|
20
|
+
if (document.getElementById(HOST_ID)) return
|
|
21
|
+
const host = document.createElement("div")
|
|
22
|
+
host.id = HOST_ID
|
|
23
|
+
// Feedback screenshots should capture the app, not platform chrome.
|
|
24
|
+
host.setAttribute("data-vibes-feedback", "")
|
|
25
|
+
document.body.appendChild(host)
|
|
26
|
+
createRoot(host).render(<OmgBadge />)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (typeof window !== "undefined") {
|
|
30
|
+
if (document.readyState === "loading") {
|
|
31
|
+
document.addEventListener("DOMContentLoaded", mount, { once: true })
|
|
32
|
+
} else {
|
|
33
|
+
mount()
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { OmgBadge, type OmgBadgeProps } from "./OmgBadge"
|