@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 CHANGED
@@ -1,5 +1,4 @@
1
- import { a as isDismissed, f as wasAutoShown, l as setAutoShown, n as detectPlatform, o as isInIframe, s as isStandalone, t as countVisit } from "./core-YJoFChtB.mjs";
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 isDismissed, c as promptNativeInstall, n as detectPlatform, o as isInIframe, s as isStandalone, t as countVisit, u as setDismissed } from "./core-YJoFChtB.mjs";
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.28",
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": {
@@ -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 };
@@ -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
- }