@omg-dev/pwa 0.4.28 → 0.4.30
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/auto.mjs +1 -2
- package/dist/index.mjs +1 -2
- package/dist/{react-Q0c2GHRr.mjs → react-Bi8T_bM4.mjs} +151 -2
- package/package.json +1 -5
- package/dist/core-YJoFChtB.mjs +0 -151
- package/dist/remix-cta.mjs +0 -141
- package/src/remix-cta.tsx +0 -194
package/dist/auto.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import { n as VibesInstallPrompt } from "./react-Q0c2GHRr.mjs";
|
|
1
|
+
import { a as countVisit, c as isInIframe, d as setAutoShown, l as isStandalone, n as VibesInstallPrompt, o as detectPlatform, p as wasAutoShown, s as isDismissed } from "./react-Bi8T_bM4.mjs";
|
|
3
2
|
import { jsx } from "react/jsx-runtime";
|
|
4
3
|
import { createRoot } from "react-dom/client";
|
|
5
4
|
//#region src/auto.tsx
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import { i as useInstallPrompt, n as VibesInstallPrompt, r as defaultAppName, t as InstallGuide } from "./react-Q0c2GHRr.mjs";
|
|
1
|
+
import { a as countVisit, c as isInIframe, f as setDismissed, i as useInstallPrompt, l as isStandalone, n as VibesInstallPrompt, o as detectPlatform, r as defaultAppName, s as isDismissed, t as InstallGuide, u as promptNativeInstall } from "./react-Bi8T_bM4.mjs";
|
|
3
2
|
export { InstallGuide, VibesInstallPrompt, countVisit, defaultAppName, detectPlatform, isDismissed, isInIframe, isStandalone, promptNativeInstall, setDismissed, useInstallPrompt };
|
|
@@ -1,7 +1,156 @@
|
|
|
1
|
-
import { a as isDismissed, c as promptNativeInstall, d as subscribeInstallState, i as getServerInstallState, n as detectPlatform, o as isInIframe, r as getInstallState, u as setDismissed } from "./core-YJoFChtB.mjs";
|
|
2
1
|
import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from "react";
|
|
3
2
|
import { createPortal } from "react-dom";
|
|
4
3
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
//#region src/core.ts
|
|
5
|
+
function detectPlatform() {
|
|
6
|
+
if (typeof navigator === "undefined") return "unsupported";
|
|
7
|
+
const ua = navigator.userAgent;
|
|
8
|
+
if (/iPad|iPhone|iPod/.test(ua) || /Mac/.test(ua) && navigator.maxTouchPoints > 1) {
|
|
9
|
+
if (/CriOS/.test(ua)) return "ios-chrome";
|
|
10
|
+
if (/FxiOS|EdgiOS|OPiOS|OPT\//.test(ua)) return "ios-other";
|
|
11
|
+
if (/Safari/.test(ua)) return "ios-safari";
|
|
12
|
+
return "unsupported";
|
|
13
|
+
}
|
|
14
|
+
if (/Android/.test(ua)) {
|
|
15
|
+
if (/; wv\)/.test(ua)) return "unsupported";
|
|
16
|
+
return "android";
|
|
17
|
+
}
|
|
18
|
+
if (/Edg\//.test(ua)) return "desktop-chrome";
|
|
19
|
+
if (/Chrome|Chromium/.test(ua)) return "desktop-chrome";
|
|
20
|
+
if (/Safari/.test(ua)) return "desktop-safari";
|
|
21
|
+
return "unsupported";
|
|
22
|
+
}
|
|
23
|
+
function isStandalone() {
|
|
24
|
+
if (typeof window === "undefined") return false;
|
|
25
|
+
if (window.matchMedia?.("(display-mode: standalone)").matches) return true;
|
|
26
|
+
return navigator.standalone === true;
|
|
27
|
+
}
|
|
28
|
+
/** True inside any iframe — e.g. the omg dashboard's preview pane. The
|
|
29
|
+
* install UI must never render there: the iframe URL is not the app URL. */
|
|
30
|
+
function isInIframe() {
|
|
31
|
+
try {
|
|
32
|
+
return window.self !== window.top;
|
|
33
|
+
} catch {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const SERVER_SNAPSHOT = {
|
|
38
|
+
canPrompt: false,
|
|
39
|
+
installed: false
|
|
40
|
+
};
|
|
41
|
+
function getStore() {
|
|
42
|
+
if (typeof window === "undefined") return null;
|
|
43
|
+
const w = window;
|
|
44
|
+
if (!w.__vibesPwaStore) {
|
|
45
|
+
const store = {
|
|
46
|
+
bip: null,
|
|
47
|
+
snapshot: {
|
|
48
|
+
canPrompt: false,
|
|
49
|
+
installed: isStandalone()
|
|
50
|
+
},
|
|
51
|
+
subs: /* @__PURE__ */ new Set()
|
|
52
|
+
};
|
|
53
|
+
w.__vibesPwaStore = store;
|
|
54
|
+
window.addEventListener("beforeinstallprompt", (e) => {
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
store.bip = e;
|
|
57
|
+
publish(store);
|
|
58
|
+
});
|
|
59
|
+
window.addEventListener("appinstalled", () => {
|
|
60
|
+
store.bip = null;
|
|
61
|
+
store.snapshot = {
|
|
62
|
+
canPrompt: false,
|
|
63
|
+
installed: true
|
|
64
|
+
};
|
|
65
|
+
for (const cb of store.subs) cb();
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return w.__vibesPwaStore;
|
|
69
|
+
}
|
|
70
|
+
function publish(store) {
|
|
71
|
+
store.snapshot = {
|
|
72
|
+
canPrompt: !!store.bip,
|
|
73
|
+
installed: store.snapshot.installed
|
|
74
|
+
};
|
|
75
|
+
for (const cb of store.subs) cb();
|
|
76
|
+
}
|
|
77
|
+
getStore();
|
|
78
|
+
function subscribeInstallState(cb) {
|
|
79
|
+
const store = getStore();
|
|
80
|
+
if (!store) return () => {};
|
|
81
|
+
store.subs.add(cb);
|
|
82
|
+
return () => store.subs.delete(cb);
|
|
83
|
+
}
|
|
84
|
+
function getInstallState() {
|
|
85
|
+
return getStore()?.snapshot ?? SERVER_SNAPSHOT;
|
|
86
|
+
}
|
|
87
|
+
function getServerInstallState() {
|
|
88
|
+
return SERVER_SNAPSHOT;
|
|
89
|
+
}
|
|
90
|
+
/** Fire the captured native install prompt (Chromium only). Resolves
|
|
91
|
+
* "unavailable" when no prompt was captured OR prompt() itself fails
|
|
92
|
+
* (emulated/headless contexts) — callers fall back to the guide. */
|
|
93
|
+
async function promptNativeInstall() {
|
|
94
|
+
const store = getStore();
|
|
95
|
+
if (!store?.bip) return "unavailable";
|
|
96
|
+
const ev = store.bip;
|
|
97
|
+
store.bip = null;
|
|
98
|
+
try {
|
|
99
|
+
await ev.prompt();
|
|
100
|
+
const { outcome } = await ev.userChoice;
|
|
101
|
+
if (outcome === "accepted") store.snapshot = {
|
|
102
|
+
canPrompt: false,
|
|
103
|
+
installed: true
|
|
104
|
+
};
|
|
105
|
+
publish(store);
|
|
106
|
+
return outcome;
|
|
107
|
+
} catch {
|
|
108
|
+
publish(store);
|
|
109
|
+
return "unavailable";
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const KEY_DISMISSED = "vibes-pwa:dismissed";
|
|
113
|
+
const KEY_VISITS = "vibes-pwa:visits";
|
|
114
|
+
const KEY_AUTO_SHOWN = "vibes-pwa:auto-shown";
|
|
115
|
+
const SESSION_COUNTED = "vibes-pwa:counted";
|
|
116
|
+
function lsGet(key) {
|
|
117
|
+
try {
|
|
118
|
+
return localStorage.getItem(key);
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function lsSet(key, value) {
|
|
124
|
+
try {
|
|
125
|
+
localStorage.setItem(key, value);
|
|
126
|
+
} catch {}
|
|
127
|
+
}
|
|
128
|
+
function isDismissed() {
|
|
129
|
+
return lsGet(KEY_DISMISSED) === "1";
|
|
130
|
+
}
|
|
131
|
+
function setDismissed() {
|
|
132
|
+
lsSet(KEY_DISMISSED, "1");
|
|
133
|
+
}
|
|
134
|
+
function wasAutoShown() {
|
|
135
|
+
return lsGet(KEY_AUTO_SHOWN) === "1";
|
|
136
|
+
}
|
|
137
|
+
function setAutoShown() {
|
|
138
|
+
lsSet(KEY_AUTO_SHOWN, "1");
|
|
139
|
+
}
|
|
140
|
+
/** Count one visit per browser session; returns the running total. */
|
|
141
|
+
function countVisit() {
|
|
142
|
+
const prev = Number(lsGet(KEY_VISITS) ?? "0") || 0;
|
|
143
|
+
try {
|
|
144
|
+
if (sessionStorage.getItem(SESSION_COUNTED) === "1") return prev;
|
|
145
|
+
sessionStorage.setItem(SESSION_COUNTED, "1");
|
|
146
|
+
} catch {
|
|
147
|
+
return prev + 1;
|
|
148
|
+
}
|
|
149
|
+
const next = prev + 1;
|
|
150
|
+
lsSet(KEY_VISITS, String(next));
|
|
151
|
+
return next;
|
|
152
|
+
}
|
|
153
|
+
//#endregion
|
|
5
154
|
//#region src/styles.ts
|
|
6
155
|
const STYLE_ID = "vibes-pwa-styles";
|
|
7
156
|
function ensureStyles() {
|
|
@@ -723,4 +872,4 @@ function VibesInstallPrompt({ appName, variant = "inline", className }) {
|
|
|
723
872
|
})] });
|
|
724
873
|
}
|
|
725
874
|
//#endregion
|
|
726
|
-
export { useInstallPrompt as i, VibesInstallPrompt as n, defaultAppName as r, InstallGuide as t };
|
|
875
|
+
export { countVisit as a, isInIframe as c, setAutoShown as d, setDismissed as f, useInstallPrompt as i, isStandalone as l, VibesInstallPrompt as n, detectPlatform as o, wasAutoShown as p, defaultAppName as r, isDismissed as s, InstallGuide as t, promptNativeInstall as u };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omg-dev/pwa",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.30",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -10,10 +10,6 @@
|
|
|
10
10
|
"./auto": {
|
|
11
11
|
"types": "./src/auto.tsx",
|
|
12
12
|
"default": "./dist/auto.mjs"
|
|
13
|
-
},
|
|
14
|
-
"./remix-cta": {
|
|
15
|
-
"types": "./src/remix-cta.tsx",
|
|
16
|
-
"default": "./dist/remix-cta.mjs"
|
|
17
13
|
}
|
|
18
14
|
},
|
|
19
15
|
"peerDependencies": {
|
package/dist/core-YJoFChtB.mjs
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
//#region src/core.ts
|
|
2
|
-
function detectPlatform() {
|
|
3
|
-
if (typeof navigator === "undefined") return "unsupported";
|
|
4
|
-
const ua = navigator.userAgent;
|
|
5
|
-
if (/iPad|iPhone|iPod/.test(ua) || /Mac/.test(ua) && navigator.maxTouchPoints > 1) {
|
|
6
|
-
if (/CriOS/.test(ua)) return "ios-chrome";
|
|
7
|
-
if (/FxiOS|EdgiOS|OPiOS|OPT\//.test(ua)) return "ios-other";
|
|
8
|
-
if (/Safari/.test(ua)) return "ios-safari";
|
|
9
|
-
return "unsupported";
|
|
10
|
-
}
|
|
11
|
-
if (/Android/.test(ua)) {
|
|
12
|
-
if (/; wv\)/.test(ua)) return "unsupported";
|
|
13
|
-
return "android";
|
|
14
|
-
}
|
|
15
|
-
if (/Edg\//.test(ua)) return "desktop-chrome";
|
|
16
|
-
if (/Chrome|Chromium/.test(ua)) return "desktop-chrome";
|
|
17
|
-
if (/Safari/.test(ua)) return "desktop-safari";
|
|
18
|
-
return "unsupported";
|
|
19
|
-
}
|
|
20
|
-
function isStandalone() {
|
|
21
|
-
if (typeof window === "undefined") return false;
|
|
22
|
-
if (window.matchMedia?.("(display-mode: standalone)").matches) return true;
|
|
23
|
-
return navigator.standalone === true;
|
|
24
|
-
}
|
|
25
|
-
/** True inside any iframe — e.g. the omg dashboard's preview pane. The
|
|
26
|
-
* install UI must never render there: the iframe URL is not the app URL. */
|
|
27
|
-
function isInIframe() {
|
|
28
|
-
try {
|
|
29
|
-
return window.self !== window.top;
|
|
30
|
-
} catch {
|
|
31
|
-
return true;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
const SERVER_SNAPSHOT = {
|
|
35
|
-
canPrompt: false,
|
|
36
|
-
installed: false
|
|
37
|
-
};
|
|
38
|
-
function getStore() {
|
|
39
|
-
if (typeof window === "undefined") return null;
|
|
40
|
-
const w = window;
|
|
41
|
-
if (!w.__vibesPwaStore) {
|
|
42
|
-
const store = {
|
|
43
|
-
bip: null,
|
|
44
|
-
snapshot: {
|
|
45
|
-
canPrompt: false,
|
|
46
|
-
installed: isStandalone()
|
|
47
|
-
},
|
|
48
|
-
subs: /* @__PURE__ */ new Set()
|
|
49
|
-
};
|
|
50
|
-
w.__vibesPwaStore = store;
|
|
51
|
-
window.addEventListener("beforeinstallprompt", (e) => {
|
|
52
|
-
e.preventDefault();
|
|
53
|
-
store.bip = e;
|
|
54
|
-
publish(store);
|
|
55
|
-
});
|
|
56
|
-
window.addEventListener("appinstalled", () => {
|
|
57
|
-
store.bip = null;
|
|
58
|
-
store.snapshot = {
|
|
59
|
-
canPrompt: false,
|
|
60
|
-
installed: true
|
|
61
|
-
};
|
|
62
|
-
for (const cb of store.subs) cb();
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
return w.__vibesPwaStore;
|
|
66
|
-
}
|
|
67
|
-
function publish(store) {
|
|
68
|
-
store.snapshot = {
|
|
69
|
-
canPrompt: !!store.bip,
|
|
70
|
-
installed: store.snapshot.installed
|
|
71
|
-
};
|
|
72
|
-
for (const cb of store.subs) cb();
|
|
73
|
-
}
|
|
74
|
-
getStore();
|
|
75
|
-
function subscribeInstallState(cb) {
|
|
76
|
-
const store = getStore();
|
|
77
|
-
if (!store) return () => {};
|
|
78
|
-
store.subs.add(cb);
|
|
79
|
-
return () => store.subs.delete(cb);
|
|
80
|
-
}
|
|
81
|
-
function getInstallState() {
|
|
82
|
-
return getStore()?.snapshot ?? SERVER_SNAPSHOT;
|
|
83
|
-
}
|
|
84
|
-
function getServerInstallState() {
|
|
85
|
-
return SERVER_SNAPSHOT;
|
|
86
|
-
}
|
|
87
|
-
/** Fire the captured native install prompt (Chromium only). Resolves
|
|
88
|
-
* "unavailable" when no prompt was captured OR prompt() itself fails
|
|
89
|
-
* (emulated/headless contexts) — callers fall back to the guide. */
|
|
90
|
-
async function promptNativeInstall() {
|
|
91
|
-
const store = getStore();
|
|
92
|
-
if (!store?.bip) return "unavailable";
|
|
93
|
-
const ev = store.bip;
|
|
94
|
-
store.bip = null;
|
|
95
|
-
try {
|
|
96
|
-
await ev.prompt();
|
|
97
|
-
const { outcome } = await ev.userChoice;
|
|
98
|
-
if (outcome === "accepted") store.snapshot = {
|
|
99
|
-
canPrompt: false,
|
|
100
|
-
installed: true
|
|
101
|
-
};
|
|
102
|
-
publish(store);
|
|
103
|
-
return outcome;
|
|
104
|
-
} catch {
|
|
105
|
-
publish(store);
|
|
106
|
-
return "unavailable";
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
const KEY_DISMISSED = "vibes-pwa:dismissed";
|
|
110
|
-
const KEY_VISITS = "vibes-pwa:visits";
|
|
111
|
-
const KEY_AUTO_SHOWN = "vibes-pwa:auto-shown";
|
|
112
|
-
const SESSION_COUNTED = "vibes-pwa:counted";
|
|
113
|
-
function lsGet(key) {
|
|
114
|
-
try {
|
|
115
|
-
return localStorage.getItem(key);
|
|
116
|
-
} catch {
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
function lsSet(key, value) {
|
|
121
|
-
try {
|
|
122
|
-
localStorage.setItem(key, value);
|
|
123
|
-
} catch {}
|
|
124
|
-
}
|
|
125
|
-
function isDismissed() {
|
|
126
|
-
return lsGet(KEY_DISMISSED) === "1";
|
|
127
|
-
}
|
|
128
|
-
function setDismissed() {
|
|
129
|
-
lsSet(KEY_DISMISSED, "1");
|
|
130
|
-
}
|
|
131
|
-
function wasAutoShown() {
|
|
132
|
-
return lsGet(KEY_AUTO_SHOWN) === "1";
|
|
133
|
-
}
|
|
134
|
-
function setAutoShown() {
|
|
135
|
-
lsSet(KEY_AUTO_SHOWN, "1");
|
|
136
|
-
}
|
|
137
|
-
/** Count one visit per browser session; returns the running total. */
|
|
138
|
-
function countVisit() {
|
|
139
|
-
const prev = Number(lsGet(KEY_VISITS) ?? "0") || 0;
|
|
140
|
-
try {
|
|
141
|
-
if (sessionStorage.getItem(SESSION_COUNTED) === "1") return prev;
|
|
142
|
-
sessionStorage.setItem(SESSION_COUNTED, "1");
|
|
143
|
-
} catch {
|
|
144
|
-
return prev + 1;
|
|
145
|
-
}
|
|
146
|
-
const next = prev + 1;
|
|
147
|
-
lsSet(KEY_VISITS, String(next));
|
|
148
|
-
return next;
|
|
149
|
-
}
|
|
150
|
-
//#endregion
|
|
151
|
-
export { isDismissed as a, promptNativeInstall as c, subscribeInstallState as d, wasAutoShown as f, getServerInstallState as i, setAutoShown as l, detectPlatform as n, isInIframe as o, getInstallState as r, isStandalone as s, countVisit as t, setDismissed as u };
|
package/dist/remix-cta.mjs
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import { o as isInIframe } from "./core-YJoFChtB.mjs";
|
|
2
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { createRoot } from "react-dom/client";
|
|
4
|
-
//#region src/remix-cta.tsx
|
|
5
|
-
const DISMISS_KEY = "vibes-remix:dismissed";
|
|
6
|
-
const REMIX_ORIGIN = typeof import.meta !== "undefined" && import.meta.env?.VITE_REMIX_ORIGIN || "https://app.omg.dev";
|
|
7
|
-
const CONTROLPLANE_URL = (typeof import.meta !== "undefined" && import.meta.env?.VITE_CONTROLPLANE_URL || "https://backend.omg.dev").replace(/\/$/, "");
|
|
8
|
-
function appSlug() {
|
|
9
|
-
if (typeof import.meta === "undefined") return null;
|
|
10
|
-
const slug = import.meta.env?.VITE_APP_SLUG?.trim();
|
|
11
|
-
if (!slug || !/^[a-z0-9-]+$/.test(slug)) return null;
|
|
12
|
-
return slug;
|
|
13
|
-
}
|
|
14
|
-
function isDismissed() {
|
|
15
|
-
try {
|
|
16
|
-
return localStorage.getItem(DISMISS_KEY) === "1";
|
|
17
|
-
} catch {
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
function setDismissed() {
|
|
22
|
-
try {
|
|
23
|
-
localStorage.setItem(DISMISS_KEY, "1");
|
|
24
|
-
} catch {}
|
|
25
|
-
}
|
|
26
|
-
/** Live check against the public catalog. Fail closed on network/error. */
|
|
27
|
-
async function isRemixable(slug) {
|
|
28
|
-
try {
|
|
29
|
-
const res = await fetch(`${CONTROLPLANE_URL}/api/projects/isRemixable`, {
|
|
30
|
-
method: "POST",
|
|
31
|
-
headers: { "content-type": "application/json" },
|
|
32
|
-
body: JSON.stringify({ slug }),
|
|
33
|
-
cache: "no-store"
|
|
34
|
-
});
|
|
35
|
-
if (!res.ok) return false;
|
|
36
|
-
return (await res.json())?.remixable === true;
|
|
37
|
-
} catch {
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
const STYLE_ID = "vibes-remix-cta-styles";
|
|
42
|
-
function ensureStyles() {
|
|
43
|
-
if (typeof document === "undefined") return;
|
|
44
|
-
if (document.getElementById(STYLE_ID)) return;
|
|
45
|
-
const el = document.createElement("style");
|
|
46
|
-
el.id = STYLE_ID;
|
|
47
|
-
el.textContent = `
|
|
48
|
-
.vremix {
|
|
49
|
-
position: fixed;
|
|
50
|
-
right: max(16px, env(safe-area-inset-right));
|
|
51
|
-
bottom: max(16px, env(safe-area-inset-bottom));
|
|
52
|
-
z-index: 2147482000;
|
|
53
|
-
display: inline-flex;
|
|
54
|
-
align-items: center;
|
|
55
|
-
gap: 8px;
|
|
56
|
-
padding: 9px 12px 9px 14px;
|
|
57
|
-
border-radius: 9999px;
|
|
58
|
-
font: 500 13px/1 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
59
|
-
color: #fff;
|
|
60
|
-
background: #18181b;
|
|
61
|
-
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
62
|
-
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.22);
|
|
63
|
-
text-decoration: none;
|
|
64
|
-
opacity: 0;
|
|
65
|
-
transform: translateY(8px);
|
|
66
|
-
transition: opacity 0.3s ease, transform 0.3s ease, background 0.15s ease;
|
|
67
|
-
-webkit-tap-highlight-color: transparent;
|
|
68
|
-
}
|
|
69
|
-
.vremix.vremix-in { opacity: 1; transform: translateY(0); }
|
|
70
|
-
.vremix:hover { background: #000; }
|
|
71
|
-
.vremix-spark { width: 14px; height: 14px; display: block; }
|
|
72
|
-
.vremix-close {
|
|
73
|
-
display: inline-flex; align-items: center; justify-content: center;
|
|
74
|
-
width: 18px; height: 18px; margin-left: 2px;
|
|
75
|
-
border-radius: 9999px; border: 0; cursor: pointer;
|
|
76
|
-
background: rgba(255, 255, 255, 0.14); color: #fff;
|
|
77
|
-
font: 600 12px/1 system-ui, sans-serif;
|
|
78
|
-
}
|
|
79
|
-
.vremix-close:hover { background: rgba(255, 255, 255, 0.26); }
|
|
80
|
-
@media (prefers-color-scheme: light) {
|
|
81
|
-
.vremix { background: #18181b; }
|
|
82
|
-
}`;
|
|
83
|
-
document.head.appendChild(el);
|
|
84
|
-
}
|
|
85
|
-
function RemixCta({ slug }) {
|
|
86
|
-
return /* @__PURE__ */ jsxs("a", {
|
|
87
|
-
className: "vremix",
|
|
88
|
-
href: `${REMIX_ORIGIN}/remix/${slug}?ref=pwa`,
|
|
89
|
-
ref: (node) => {
|
|
90
|
-
if (node) requestAnimationFrame(() => node.classList.add("vremix-in"));
|
|
91
|
-
},
|
|
92
|
-
children: [
|
|
93
|
-
/* @__PURE__ */ jsx("svg", {
|
|
94
|
-
className: "vremix-spark",
|
|
95
|
-
viewBox: "0 0 24 24",
|
|
96
|
-
fill: "none",
|
|
97
|
-
"aria-hidden": "true",
|
|
98
|
-
children: /* @__PURE__ */ jsx("path", {
|
|
99
|
-
d: "M12 3l1.9 4.6L18.5 9.5 13.9 11.4 12 16l-1.9-4.6L5.5 9.5l4.6-1.9L12 3z",
|
|
100
|
-
fill: "currentColor"
|
|
101
|
-
})
|
|
102
|
-
}),
|
|
103
|
-
"Make it mine",
|
|
104
|
-
/* @__PURE__ */ jsx("button", {
|
|
105
|
-
className: "vremix-close",
|
|
106
|
-
"aria-label": "Dismiss",
|
|
107
|
-
onClick: (e) => {
|
|
108
|
-
e.preventDefault();
|
|
109
|
-
e.stopPropagation();
|
|
110
|
-
setDismissed();
|
|
111
|
-
const host = document.getElementById("vibes-remix-cta");
|
|
112
|
-
if (host) host.remove();
|
|
113
|
-
},
|
|
114
|
-
children: "×"
|
|
115
|
-
})
|
|
116
|
-
]
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
function shouldShow() {
|
|
120
|
-
if (typeof window === "undefined" || typeof document === "undefined") return false;
|
|
121
|
-
if (isInIframe()) return false;
|
|
122
|
-
if (isDismissed()) return false;
|
|
123
|
-
return appSlug() !== null;
|
|
124
|
-
}
|
|
125
|
-
async function mount() {
|
|
126
|
-
if (!shouldShow()) return;
|
|
127
|
-
const slug = appSlug();
|
|
128
|
-
if (!slug) return;
|
|
129
|
-
if (!await isRemixable(slug)) return;
|
|
130
|
-
if (isDismissed()) return;
|
|
131
|
-
if (document.getElementById("vibes-remix-cta")) return;
|
|
132
|
-
ensureStyles();
|
|
133
|
-
const host = document.createElement("div");
|
|
134
|
-
host.id = "vibes-remix-cta";
|
|
135
|
-
document.body.appendChild(host);
|
|
136
|
-
createRoot(host).render(/* @__PURE__ */ jsx(RemixCta, { slug }));
|
|
137
|
-
}
|
|
138
|
-
if (typeof window !== "undefined") if (document.readyState === "complete") setTimeout(() => void mount(), 1800);
|
|
139
|
-
else window.addEventListener("load", () => setTimeout(() => void mount(), 1800), { once: true });
|
|
140
|
-
//#endregion
|
|
141
|
-
export {};
|
package/src/remix-cta.tsx
DELETED
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
// @omg-dev/pwa/remix-cta — the "Make your own" bridge baked into PUBLISHED apps.
|
|
2
|
-
//
|
|
3
|
-
// A small floating pill in the bottom corner of a deployed user app
|
|
4
|
-
// (https://<slug>.apps.omg.dev) that deep-links back to omg.dev to remix THAT
|
|
5
|
-
// app: the recipient->creator hinge (GOAL.md) for visitors who only have the
|
|
6
|
-
// shared link. Like @omg-dev/pwa/auto it mounts OUTSIDE the app's React tree
|
|
7
|
-
// (own root + DOM node) so it survives whatever the build agent did to
|
|
8
|
-
// App.tsx/main.tsx, and the vite-plugin appends the import on BUILD only — it
|
|
9
|
-
// never loads in dev/preview.
|
|
10
|
-
//
|
|
11
|
-
// Policy (deliberately quiet):
|
|
12
|
-
// - never in an iframe (the dashboard preview pane is not the app URL)
|
|
13
|
-
// - only when import.meta.env.VITE_APP_SLUG is baked in (prod builds only)
|
|
14
|
-
// - only when the app is currently listed public (isPublic) — private apps
|
|
15
|
-
// keep no CTA; remixBySlug rejects them
|
|
16
|
-
// - dismissable forever (per-origin localStorage)
|
|
17
|
-
|
|
18
|
-
import { createRoot } from "react-dom/client"
|
|
19
|
-
import { isInIframe } from "./core"
|
|
20
|
-
|
|
21
|
-
const DISMISS_KEY = "vibes-remix:dismissed"
|
|
22
|
-
// The dashboard origin the remix deep-link points at. Overridable for
|
|
23
|
-
// self-host via VITE_REMIX_ORIGIN.
|
|
24
|
-
//
|
|
25
|
-
// app.omg.dev, NOT omg.dev: /remix/<slug> is a dashboard SPA route, and the
|
|
26
|
-
// dashboard moved off omg.dev when that host became the marketing landing.
|
|
27
|
-
// The landing has no /remix route, so the old default sent every recipient of
|
|
28
|
-
// a shared app to a "Not Found" page — the whole recipient->creator loop.
|
|
29
|
-
const REMIX_ORIGIN =
|
|
30
|
-
(typeof import.meta !== "undefined" &&
|
|
31
|
-
(import.meta as { env?: Record<string, string | undefined> }).env?.VITE_REMIX_ORIGIN) ||
|
|
32
|
-
"https://app.omg.dev"
|
|
33
|
-
const CONTROLPLANE_URL = (
|
|
34
|
-
(typeof import.meta !== "undefined" &&
|
|
35
|
-
(import.meta as { env?: Record<string, string | undefined> }).env?.VITE_CONTROLPLANE_URL) ||
|
|
36
|
-
"https://backend.omg.dev"
|
|
37
|
-
).replace(/\/$/, "")
|
|
38
|
-
|
|
39
|
-
function appSlug(): string | null {
|
|
40
|
-
if (typeof import.meta === "undefined") return null
|
|
41
|
-
const env = (import.meta as { env?: Record<string, string | undefined> }).env
|
|
42
|
-
const slug = env?.VITE_APP_SLUG?.trim()
|
|
43
|
-
if (!slug || !/^[a-z0-9-]+$/.test(slug)) return null
|
|
44
|
-
return slug
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function isDismissed(): boolean {
|
|
48
|
-
try {
|
|
49
|
-
return localStorage.getItem(DISMISS_KEY) === "1"
|
|
50
|
-
} catch {
|
|
51
|
-
return false
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function setDismissed() {
|
|
56
|
-
try {
|
|
57
|
-
localStorage.setItem(DISMISS_KEY, "1")
|
|
58
|
-
} catch {
|
|
59
|
-
/* private mode */
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** Live check against the public catalog. Fail closed on network/error. */
|
|
64
|
-
async function isRemixable(slug: string): Promise<boolean> {
|
|
65
|
-
try {
|
|
66
|
-
const res = await fetch(`${CONTROLPLANE_URL}/api/projects/isRemixable`, {
|
|
67
|
-
method: "POST",
|
|
68
|
-
headers: { "content-type": "application/json" },
|
|
69
|
-
body: JSON.stringify({ slug }),
|
|
70
|
-
cache: "no-store",
|
|
71
|
-
})
|
|
72
|
-
if (!res.ok) return false
|
|
73
|
-
const data = (await res.json()) as { remixable?: unknown }
|
|
74
|
-
return data?.remixable === true
|
|
75
|
-
} catch {
|
|
76
|
-
return false
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const STYLE_ID = "vibes-remix-cta-styles"
|
|
81
|
-
function ensureStyles() {
|
|
82
|
-
if (typeof document === "undefined") return
|
|
83
|
-
if (document.getElementById(STYLE_ID)) return
|
|
84
|
-
const el = document.createElement("style")
|
|
85
|
-
el.id = STYLE_ID
|
|
86
|
-
el.textContent = `
|
|
87
|
-
.vremix {
|
|
88
|
-
position: fixed;
|
|
89
|
-
right: max(16px, env(safe-area-inset-right));
|
|
90
|
-
bottom: max(16px, env(safe-area-inset-bottom));
|
|
91
|
-
z-index: 2147482000;
|
|
92
|
-
display: inline-flex;
|
|
93
|
-
align-items: center;
|
|
94
|
-
gap: 8px;
|
|
95
|
-
padding: 9px 12px 9px 14px;
|
|
96
|
-
border-radius: 9999px;
|
|
97
|
-
font: 500 13px/1 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
98
|
-
color: #fff;
|
|
99
|
-
background: #18181b;
|
|
100
|
-
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
101
|
-
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.22);
|
|
102
|
-
text-decoration: none;
|
|
103
|
-
opacity: 0;
|
|
104
|
-
transform: translateY(8px);
|
|
105
|
-
transition: opacity 0.3s ease, transform 0.3s ease, background 0.15s ease;
|
|
106
|
-
-webkit-tap-highlight-color: transparent;
|
|
107
|
-
}
|
|
108
|
-
.vremix.vremix-in { opacity: 1; transform: translateY(0); }
|
|
109
|
-
.vremix:hover { background: #000; }
|
|
110
|
-
.vremix-spark { width: 14px; height: 14px; display: block; }
|
|
111
|
-
.vremix-close {
|
|
112
|
-
display: inline-flex; align-items: center; justify-content: center;
|
|
113
|
-
width: 18px; height: 18px; margin-left: 2px;
|
|
114
|
-
border-radius: 9999px; border: 0; cursor: pointer;
|
|
115
|
-
background: rgba(255, 255, 255, 0.14); color: #fff;
|
|
116
|
-
font: 600 12px/1 system-ui, sans-serif;
|
|
117
|
-
}
|
|
118
|
-
.vremix-close:hover { background: rgba(255, 255, 255, 0.26); }
|
|
119
|
-
@media (prefers-color-scheme: light) {
|
|
120
|
-
.vremix { background: #18181b; }
|
|
121
|
-
}`
|
|
122
|
-
document.head.appendChild(el)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function RemixCta({ slug }: { slug: string }) {
|
|
126
|
-
// ?ref=pwa attributes the click on the dashboard's /remix landing — the
|
|
127
|
-
// published app has no omg.dev analytics, so the landing is the only place
|
|
128
|
-
// this surface can be counted (see remix_cta_click in apps/web).
|
|
129
|
-
const href = `${REMIX_ORIGIN}/remix/${slug}?ref=pwa`
|
|
130
|
-
return (
|
|
131
|
-
<a
|
|
132
|
-
className="vremix"
|
|
133
|
-
href={href}
|
|
134
|
-
// Top-level navigation to the dashboard — the app sits on a different
|
|
135
|
-
// origin (<slug>.apps.omg.dev), so this is a plain cross-origin link.
|
|
136
|
-
ref={(node) => {
|
|
137
|
-
if (node) requestAnimationFrame(() => node.classList.add("vremix-in"))
|
|
138
|
-
}}
|
|
139
|
-
>
|
|
140
|
-
<svg className="vremix-spark" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
141
|
-
<path
|
|
142
|
-
d="M12 3l1.9 4.6L18.5 9.5 13.9 11.4 12 16l-1.9-4.6L5.5 9.5l4.6-1.9L12 3z"
|
|
143
|
-
fill="currentColor"
|
|
144
|
-
/>
|
|
145
|
-
</svg>
|
|
146
|
-
Make it mine
|
|
147
|
-
<button
|
|
148
|
-
className="vremix-close"
|
|
149
|
-
aria-label="Dismiss"
|
|
150
|
-
onClick={(e) => {
|
|
151
|
-
e.preventDefault()
|
|
152
|
-
e.stopPropagation()
|
|
153
|
-
setDismissed()
|
|
154
|
-
const host = document.getElementById("vibes-remix-cta")
|
|
155
|
-
if (host) host.remove()
|
|
156
|
-
}}
|
|
157
|
-
>
|
|
158
|
-
×
|
|
159
|
-
</button>
|
|
160
|
-
</a>
|
|
161
|
-
)
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function shouldShow(): boolean {
|
|
165
|
-
if (typeof window === "undefined" || typeof document === "undefined") return false
|
|
166
|
-
if (isInIframe()) return false
|
|
167
|
-
if (isDismissed()) return false
|
|
168
|
-
return appSlug() !== null
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
async function mount() {
|
|
172
|
-
if (!shouldShow()) return
|
|
173
|
-
const slug = appSlug()
|
|
174
|
-
if (!slug) return
|
|
175
|
-
// Only mount for Explore-listed public apps. Private published apps get no
|
|
176
|
-
// CTA (branding lives on the omg badge when that path is enabled).
|
|
177
|
-
if (!(await isRemixable(slug))) return
|
|
178
|
-
if (isDismissed()) return
|
|
179
|
-
if (document.getElementById("vibes-remix-cta")) return
|
|
180
|
-
ensureStyles()
|
|
181
|
-
const host = document.createElement("div")
|
|
182
|
-
host.id = "vibes-remix-cta"
|
|
183
|
-
document.body.appendChild(host)
|
|
184
|
-
createRoot(host).render(<RemixCta slug={slug} />)
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (typeof window !== "undefined") {
|
|
188
|
-
// Let the app paint first — the CTA is an afterthought, not a gate.
|
|
189
|
-
if (document.readyState === "complete") {
|
|
190
|
-
setTimeout(() => void mount(), 1800)
|
|
191
|
-
} else {
|
|
192
|
-
window.addEventListener("load", () => setTimeout(() => void mount(), 1800), { once: true })
|
|
193
|
-
}
|
|
194
|
-
}
|