@omg-dev/pwa 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/auto.mjs +26 -0
- package/dist/core-YJoFChtB.mjs +151 -0
- package/dist/index.mjs +3 -0
- package/dist/react-Q0c2GHRr.mjs +726 -0
- package/dist/remix-cta.mjs +122 -0
- package/package.json +42 -0
- package/src/auto.tsx +55 -0
- package/src/core.ts +197 -0
- package/src/icons.tsx +134 -0
- package/src/index.ts +21 -0
- package/src/react.tsx +364 -0
- package/src/remix-cta.tsx +160 -0
- package/src/styles.ts +213 -0
|
@@ -0,0 +1,122 @@
|
|
|
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://omg.dev";
|
|
7
|
+
function appSlug() {
|
|
8
|
+
if (typeof import.meta === "undefined") return null;
|
|
9
|
+
const slug = import.meta.env?.VITE_APP_SLUG?.trim();
|
|
10
|
+
if (!slug || !/^[a-z0-9-]+$/.test(slug)) return null;
|
|
11
|
+
return slug;
|
|
12
|
+
}
|
|
13
|
+
function isDismissed() {
|
|
14
|
+
try {
|
|
15
|
+
return localStorage.getItem(DISMISS_KEY) === "1";
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function setDismissed() {
|
|
21
|
+
try {
|
|
22
|
+
localStorage.setItem(DISMISS_KEY, "1");
|
|
23
|
+
} catch {}
|
|
24
|
+
}
|
|
25
|
+
const STYLE_ID = "vibes-remix-cta-styles";
|
|
26
|
+
function ensureStyles() {
|
|
27
|
+
if (typeof document === "undefined") return;
|
|
28
|
+
if (document.getElementById(STYLE_ID)) return;
|
|
29
|
+
const el = document.createElement("style");
|
|
30
|
+
el.id = STYLE_ID;
|
|
31
|
+
el.textContent = `
|
|
32
|
+
.vremix {
|
|
33
|
+
position: fixed;
|
|
34
|
+
right: max(16px, env(safe-area-inset-right));
|
|
35
|
+
bottom: max(16px, env(safe-area-inset-bottom));
|
|
36
|
+
z-index: 2147482000;
|
|
37
|
+
display: inline-flex;
|
|
38
|
+
align-items: center;
|
|
39
|
+
gap: 8px;
|
|
40
|
+
padding: 9px 12px 9px 14px;
|
|
41
|
+
border-radius: 9999px;
|
|
42
|
+
font: 500 13px/1 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
43
|
+
color: #fff;
|
|
44
|
+
background: #18181b;
|
|
45
|
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
46
|
+
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.22);
|
|
47
|
+
text-decoration: none;
|
|
48
|
+
opacity: 0;
|
|
49
|
+
transform: translateY(8px);
|
|
50
|
+
transition: opacity 0.3s ease, transform 0.3s ease, background 0.15s ease;
|
|
51
|
+
-webkit-tap-highlight-color: transparent;
|
|
52
|
+
}
|
|
53
|
+
.vremix.vremix-in { opacity: 1; transform: translateY(0); }
|
|
54
|
+
.vremix:hover { background: #000; }
|
|
55
|
+
.vremix-spark { width: 14px; height: 14px; display: block; }
|
|
56
|
+
.vremix-close {
|
|
57
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
58
|
+
width: 18px; height: 18px; margin-left: 2px;
|
|
59
|
+
border-radius: 9999px; border: 0; cursor: pointer;
|
|
60
|
+
background: rgba(255, 255, 255, 0.14); color: #fff;
|
|
61
|
+
font: 600 12px/1 system-ui, sans-serif;
|
|
62
|
+
}
|
|
63
|
+
.vremix-close:hover { background: rgba(255, 255, 255, 0.26); }
|
|
64
|
+
@media (prefers-color-scheme: light) {
|
|
65
|
+
.vremix { background: #18181b; }
|
|
66
|
+
}`;
|
|
67
|
+
document.head.appendChild(el);
|
|
68
|
+
}
|
|
69
|
+
function RemixCta({ slug }) {
|
|
70
|
+
return /* @__PURE__ */ jsxs("a", {
|
|
71
|
+
className: "vremix",
|
|
72
|
+
href: `${REMIX_ORIGIN}/remix/${slug}?ref=pwa`,
|
|
73
|
+
ref: (node) => {
|
|
74
|
+
if (node) requestAnimationFrame(() => node.classList.add("vremix-in"));
|
|
75
|
+
},
|
|
76
|
+
children: [
|
|
77
|
+
/* @__PURE__ */ jsx("svg", {
|
|
78
|
+
className: "vremix-spark",
|
|
79
|
+
viewBox: "0 0 24 24",
|
|
80
|
+
fill: "none",
|
|
81
|
+
"aria-hidden": "true",
|
|
82
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
83
|
+
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",
|
|
84
|
+
fill: "currentColor"
|
|
85
|
+
})
|
|
86
|
+
}),
|
|
87
|
+
"Make it mine",
|
|
88
|
+
/* @__PURE__ */ jsx("button", {
|
|
89
|
+
className: "vremix-close",
|
|
90
|
+
"aria-label": "Dismiss",
|
|
91
|
+
onClick: (e) => {
|
|
92
|
+
e.preventDefault();
|
|
93
|
+
e.stopPropagation();
|
|
94
|
+
setDismissed();
|
|
95
|
+
const host = document.getElementById("vibes-remix-cta");
|
|
96
|
+
if (host) host.remove();
|
|
97
|
+
},
|
|
98
|
+
children: "×"
|
|
99
|
+
})
|
|
100
|
+
]
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function shouldShow() {
|
|
104
|
+
if (typeof window === "undefined" || typeof document === "undefined") return false;
|
|
105
|
+
if (isInIframe()) return false;
|
|
106
|
+
if (isDismissed()) return false;
|
|
107
|
+
return appSlug() !== null;
|
|
108
|
+
}
|
|
109
|
+
function mount() {
|
|
110
|
+
if (!shouldShow()) return;
|
|
111
|
+
const slug = appSlug();
|
|
112
|
+
if (!slug) return;
|
|
113
|
+
ensureStyles();
|
|
114
|
+
const host = document.createElement("div");
|
|
115
|
+
host.id = "vibes-remix-cta";
|
|
116
|
+
document.body.appendChild(host);
|
|
117
|
+
createRoot(host).render(/* @__PURE__ */ jsx(RemixCta, { slug }));
|
|
118
|
+
}
|
|
119
|
+
if (typeof window !== "undefined") if (document.readyState === "complete") setTimeout(mount, 1800);
|
|
120
|
+
else window.addEventListener("load", () => setTimeout(mount, 1800), { once: true });
|
|
121
|
+
//#endregion
|
|
122
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@omg-dev/pwa",
|
|
3
|
+
"version": "0.4.24",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"default": "./dist/index.mjs"
|
|
9
|
+
},
|
|
10
|
+
"./auto": {
|
|
11
|
+
"types": "./src/auto.tsx",
|
|
12
|
+
"default": "./dist/auto.mjs"
|
|
13
|
+
},
|
|
14
|
+
"./remix-cta": {
|
|
15
|
+
"types": "./src/remix-cta.tsx",
|
|
16
|
+
"default": "./dist/remix-cta.mjs"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"react": ">=18",
|
|
21
|
+
"react-dom": ">=18"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/react": "^19.2.14",
|
|
25
|
+
"@types/react-dom": "^19.2.3",
|
|
26
|
+
"typescript": "^5.4.5"
|
|
27
|
+
},
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/BennyKok/vibes.git"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://docs.omg.dev",
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"src"
|
|
37
|
+
],
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public",
|
|
40
|
+
"registry": "https://registry.npmjs.org/"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/auto.tsx
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// @omg-dev/pwa/auto — soft install prompt, injected into published builds by
|
|
2
|
+
// @omg-dev/vite-plugin (appended import in the app entry; see its pwa.ts).
|
|
3
|
+
//
|
|
4
|
+
// Mounts OUTSIDE the app's React tree (own root, own DOM node) so it
|
|
5
|
+
// survives whatever the build agent does to App.tsx/main.tsx. Policy is
|
|
6
|
+
// deliberately conservative:
|
|
7
|
+
// - never in an iframe (dashboard preview pane)
|
|
8
|
+
// - never when already running standalone / installed
|
|
9
|
+
// - mobile platforms only — desktop visitors aren't nagged
|
|
10
|
+
// - second visit onwards, shown ONCE; an explicit dismiss is forever
|
|
11
|
+
//
|
|
12
|
+
// Apps that want more control: vibes({ pwa: { autoPrompt: false } }) in
|
|
13
|
+
// vite.config.ts and place <VibesInstallPrompt/> from @omg-dev/pwa manually.
|
|
14
|
+
|
|
15
|
+
import { createRoot } from "react-dom/client"
|
|
16
|
+
import {
|
|
17
|
+
countVisit,
|
|
18
|
+
detectPlatform,
|
|
19
|
+
isDismissed,
|
|
20
|
+
isInIframe,
|
|
21
|
+
isStandalone,
|
|
22
|
+
setAutoShown,
|
|
23
|
+
wasAutoShown,
|
|
24
|
+
} from "./core"
|
|
25
|
+
import { VibesInstallPrompt } from "./react"
|
|
26
|
+
|
|
27
|
+
function shouldAutoShow(): boolean {
|
|
28
|
+
if (typeof window === "undefined" || typeof document === "undefined") return false
|
|
29
|
+
if (isInIframe()) return false
|
|
30
|
+
if (isStandalone()) return false
|
|
31
|
+
if (isDismissed() || wasAutoShown()) return false
|
|
32
|
+
const platform = detectPlatform()
|
|
33
|
+
if (platform !== "ios-safari" && platform !== "ios-chrome" && platform !== "ios-other" && platform !== "android") {
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
return countVisit() >= 2
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function mount() {
|
|
40
|
+
if (!shouldAutoShow()) return
|
|
41
|
+
setAutoShown()
|
|
42
|
+
const host = document.createElement("div")
|
|
43
|
+
host.id = "vibes-pwa-auto"
|
|
44
|
+
document.body.appendChild(host)
|
|
45
|
+
createRoot(host).render(<VibesInstallPrompt variant="floating" />)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (typeof window !== "undefined") {
|
|
49
|
+
// Give the app a moment to settle before sliding anything in.
|
|
50
|
+
if (document.readyState === "complete") {
|
|
51
|
+
setTimeout(mount, 2500)
|
|
52
|
+
} else {
|
|
53
|
+
window.addEventListener("load", () => setTimeout(mount, 2500), { once: true })
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/core.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// @omg-dev/pwa — platform detection + beforeinstallprompt capture.
|
|
2
|
+
//
|
|
3
|
+
// The capture listener must be installed before Chrome fires
|
|
4
|
+
// `beforeinstallprompt` (shortly after load), so this module attaches it at
|
|
5
|
+
// import time. The store lives on `window` so the auto-injected entry and an
|
|
6
|
+
// app-imported copy of this module always share one capture — a second copy
|
|
7
|
+
// evaluating is a no-op.
|
|
8
|
+
|
|
9
|
+
export interface BeforeInstallPromptEvent extends Event {
|
|
10
|
+
prompt(): Promise<void>
|
|
11
|
+
userChoice: Promise<{ outcome: "accepted" | "dismissed" }>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type InstallPlatform =
|
|
15
|
+
| "ios-safari" // Safari on iPhone/iPad — Share (toolbar) → Add to Home Screen
|
|
16
|
+
| "ios-chrome" // Chrome on iOS — Share (top bar) → Add to Home Screen
|
|
17
|
+
| "ios-other" // other iOS browsers — share sheet → Add to Home Screen (iOS ≥16.4)
|
|
18
|
+
| "android" // Chromium-family mobile — native prompt or ⋮ → Add to Home screen
|
|
19
|
+
| "desktop-chrome" // Chrome/Edge desktop — omnibox install icon
|
|
20
|
+
| "desktop-safari" // macOS Safari — File → Add to Dock
|
|
21
|
+
| "unsupported" // desktop Firefox, webviews, …
|
|
22
|
+
|
|
23
|
+
export function detectPlatform(): InstallPlatform {
|
|
24
|
+
if (typeof navigator === "undefined") return "unsupported"
|
|
25
|
+
const ua = navigator.userAgent
|
|
26
|
+
const isIOS =
|
|
27
|
+
/iPad|iPhone|iPod/.test(ua) ||
|
|
28
|
+
// iPadOS reports itself as a Mac; touch points give it away.
|
|
29
|
+
(/Mac/.test(ua) && navigator.maxTouchPoints > 1)
|
|
30
|
+
if (isIOS) {
|
|
31
|
+
if (/CriOS/.test(ua)) return "ios-chrome"
|
|
32
|
+
if (/FxiOS|EdgiOS|OPiOS|OPT\//.test(ua)) return "ios-other"
|
|
33
|
+
if (/Safari/.test(ua)) return "ios-safari"
|
|
34
|
+
// In-app webviews (Instagram, Gmail, …) have no share-sheet install row.
|
|
35
|
+
return "unsupported"
|
|
36
|
+
}
|
|
37
|
+
if (/Android/.test(ua)) {
|
|
38
|
+
// Android webviews can't install.
|
|
39
|
+
if (/; wv\)/.test(ua)) return "unsupported"
|
|
40
|
+
return "android"
|
|
41
|
+
}
|
|
42
|
+
if (/Edg\//.test(ua)) return "desktop-chrome"
|
|
43
|
+
if (/Chrome|Chromium/.test(ua)) return "desktop-chrome"
|
|
44
|
+
if (/Safari/.test(ua)) return "desktop-safari"
|
|
45
|
+
return "unsupported"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isStandalone(): boolean {
|
|
49
|
+
if (typeof window === "undefined") return false
|
|
50
|
+
if (window.matchMedia?.("(display-mode: standalone)").matches) return true
|
|
51
|
+
return (navigator as { standalone?: boolean }).standalone === true
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** True inside any iframe — e.g. the omg dashboard's preview pane. The
|
|
55
|
+
* install UI must never render there: the iframe URL is not the app URL. */
|
|
56
|
+
export function isInIframe(): boolean {
|
|
57
|
+
try {
|
|
58
|
+
return window.self !== window.top
|
|
59
|
+
} catch {
|
|
60
|
+
return true // cross-origin parent → definitely framed
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── install-prompt store (window singleton) ─────────────────────────────────
|
|
65
|
+
|
|
66
|
+
export interface InstallState {
|
|
67
|
+
canPrompt: boolean
|
|
68
|
+
installed: boolean
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface PwaStore {
|
|
72
|
+
bip: BeforeInstallPromptEvent | null
|
|
73
|
+
snapshot: InstallState
|
|
74
|
+
subs: Set<() => void>
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const SERVER_SNAPSHOT: InstallState = { canPrompt: false, installed: false }
|
|
78
|
+
|
|
79
|
+
function getStore(): PwaStore | null {
|
|
80
|
+
if (typeof window === "undefined") return null
|
|
81
|
+
const w = window as typeof window & { __vibesPwaStore?: PwaStore }
|
|
82
|
+
if (!w.__vibesPwaStore) {
|
|
83
|
+
const store: PwaStore = {
|
|
84
|
+
bip: null,
|
|
85
|
+
snapshot: { canPrompt: false, installed: isStandalone() },
|
|
86
|
+
subs: new Set(),
|
|
87
|
+
}
|
|
88
|
+
w.__vibesPwaStore = store
|
|
89
|
+
window.addEventListener("beforeinstallprompt", (e) => {
|
|
90
|
+
e.preventDefault() // suppress Chrome's mini-infobar; we prompt on tap
|
|
91
|
+
store.bip = e as BeforeInstallPromptEvent
|
|
92
|
+
publish(store)
|
|
93
|
+
})
|
|
94
|
+
window.addEventListener("appinstalled", () => {
|
|
95
|
+
store.bip = null
|
|
96
|
+
store.snapshot = { canPrompt: false, installed: true }
|
|
97
|
+
for (const cb of store.subs) cb()
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
return w.__vibesPwaStore
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function publish(store: PwaStore) {
|
|
104
|
+
store.snapshot = { canPrompt: !!store.bip, installed: store.snapshot.installed }
|
|
105
|
+
for (const cb of store.subs) cb()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Attach the capture listener as early as possible.
|
|
109
|
+
getStore()
|
|
110
|
+
|
|
111
|
+
export function subscribeInstallState(cb: () => void): () => void {
|
|
112
|
+
const store = getStore()
|
|
113
|
+
if (!store) return () => {}
|
|
114
|
+
store.subs.add(cb)
|
|
115
|
+
return () => store.subs.delete(cb)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function getInstallState(): InstallState {
|
|
119
|
+
return getStore()?.snapshot ?? SERVER_SNAPSHOT
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function getServerInstallState(): InstallState {
|
|
123
|
+
return SERVER_SNAPSHOT
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Fire the captured native install prompt (Chromium only). Resolves
|
|
127
|
+
* "unavailable" when no prompt was captured OR prompt() itself fails
|
|
128
|
+
* (emulated/headless contexts) — callers fall back to the guide. */
|
|
129
|
+
export async function promptNativeInstall(): Promise<"accepted" | "dismissed" | "unavailable"> {
|
|
130
|
+
const store = getStore()
|
|
131
|
+
if (!store?.bip) return "unavailable"
|
|
132
|
+
const ev = store.bip
|
|
133
|
+
store.bip = null
|
|
134
|
+
try {
|
|
135
|
+
await ev.prompt()
|
|
136
|
+
const { outcome } = await ev.userChoice
|
|
137
|
+
if (outcome === "accepted") store.snapshot = { canPrompt: false, installed: true }
|
|
138
|
+
publish(store)
|
|
139
|
+
return outcome
|
|
140
|
+
} catch {
|
|
141
|
+
publish(store)
|
|
142
|
+
return "unavailable"
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── persistence (per-origin localStorage) ────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
const KEY_DISMISSED = "vibes-pwa:dismissed"
|
|
149
|
+
const KEY_VISITS = "vibes-pwa:visits"
|
|
150
|
+
const KEY_AUTO_SHOWN = "vibes-pwa:auto-shown"
|
|
151
|
+
const SESSION_COUNTED = "vibes-pwa:counted"
|
|
152
|
+
|
|
153
|
+
function lsGet(key: string): string | null {
|
|
154
|
+
try {
|
|
155
|
+
return localStorage.getItem(key)
|
|
156
|
+
} catch {
|
|
157
|
+
return null // private mode / storage disabled
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function lsSet(key: string, value: string) {
|
|
162
|
+
try {
|
|
163
|
+
localStorage.setItem(key, value)
|
|
164
|
+
} catch {
|
|
165
|
+
/* private mode */
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function isDismissed(): boolean {
|
|
170
|
+
return lsGet(KEY_DISMISSED) === "1"
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function setDismissed() {
|
|
174
|
+
lsSet(KEY_DISMISSED, "1")
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function wasAutoShown(): boolean {
|
|
178
|
+
return lsGet(KEY_AUTO_SHOWN) === "1"
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function setAutoShown() {
|
|
182
|
+
lsSet(KEY_AUTO_SHOWN, "1")
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Count one visit per browser session; returns the running total. */
|
|
186
|
+
export function countVisit(): number {
|
|
187
|
+
const prev = Number(lsGet(KEY_VISITS) ?? "0") || 0
|
|
188
|
+
try {
|
|
189
|
+
if (sessionStorage.getItem(SESSION_COUNTED) === "1") return prev
|
|
190
|
+
sessionStorage.setItem(SESSION_COUNTED, "1")
|
|
191
|
+
} catch {
|
|
192
|
+
return prev + 1 // can't dedupe — count it, don't persist double
|
|
193
|
+
}
|
|
194
|
+
const next = prev + 1
|
|
195
|
+
lsSet(KEY_VISITS, String(next))
|
|
196
|
+
return next
|
|
197
|
+
}
|
package/src/icons.tsx
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// @omg-dev/pwa — inline SVG glyphs (lucide-style strokes, currentColor).
|
|
2
|
+
// Dependency-free so the package adds nothing to user-app bundles.
|
|
3
|
+
|
|
4
|
+
interface IconProps {
|
|
5
|
+
size?: number
|
|
6
|
+
className?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function svgProps({ size = 18, className }: IconProps) {
|
|
10
|
+
return {
|
|
11
|
+
width: size,
|
|
12
|
+
height: size,
|
|
13
|
+
viewBox: "0 0 24 24",
|
|
14
|
+
fill: "none",
|
|
15
|
+
stroke: "currentColor",
|
|
16
|
+
strokeWidth: 2,
|
|
17
|
+
strokeLinecap: "round" as const,
|
|
18
|
+
strokeLinejoin: "round" as const,
|
|
19
|
+
className,
|
|
20
|
+
"aria-hidden": true,
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** iOS share: square with arrow up through the top. */
|
|
25
|
+
export function ShareIcon(p: IconProps) {
|
|
26
|
+
return (
|
|
27
|
+
<svg {...svgProps(p)}>
|
|
28
|
+
<path d="M4 12v7a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-7" />
|
|
29
|
+
<polyline points="16 6 12 2 8 6" />
|
|
30
|
+
<line x1="12" y1="2" x2="12" y2="15" />
|
|
31
|
+
</svg>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function SquarePlusIcon(p: IconProps) {
|
|
36
|
+
return (
|
|
37
|
+
<svg {...svgProps(p)}>
|
|
38
|
+
<rect width="18" height="18" x="3" y="3" rx="3" />
|
|
39
|
+
<path d="M8 12h8" />
|
|
40
|
+
<path d="M12 8v8" />
|
|
41
|
+
</svg>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function EllipsisVerticalIcon(p: IconProps) {
|
|
46
|
+
return (
|
|
47
|
+
<svg {...svgProps(p)}>
|
|
48
|
+
<circle cx="12" cy="5" r="1" fill="currentColor" />
|
|
49
|
+
<circle cx="12" cy="12" r="1" fill="currentColor" />
|
|
50
|
+
<circle cx="12" cy="19" r="1" fill="currentColor" />
|
|
51
|
+
</svg>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function ChevronLeftIcon(p: IconProps) {
|
|
56
|
+
return (
|
|
57
|
+
<svg {...svgProps(p)}>
|
|
58
|
+
<path d="m15 18-6-6 6-6" />
|
|
59
|
+
</svg>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function ChevronRightIcon(p: IconProps) {
|
|
64
|
+
return (
|
|
65
|
+
<svg {...svgProps(p)}>
|
|
66
|
+
<path d="m9 18 6-6-6-6" />
|
|
67
|
+
</svg>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function BookIcon(p: IconProps) {
|
|
72
|
+
return (
|
|
73
|
+
<svg {...svgProps(p)}>
|
|
74
|
+
<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20" />
|
|
75
|
+
</svg>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function CopySquaresIcon(p: IconProps) {
|
|
80
|
+
return (
|
|
81
|
+
<svg {...svgProps(p)}>
|
|
82
|
+
<rect width="14" height="14" x="8" y="8" rx="2" />
|
|
83
|
+
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
|
|
84
|
+
</svg>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Chrome's omnibox install glyph: monitor with a down arrow. */
|
|
89
|
+
export function MonitorDownIcon(p: IconProps) {
|
|
90
|
+
return (
|
|
91
|
+
<svg {...svgProps(p)}>
|
|
92
|
+
<path d="M12 13V7" />
|
|
93
|
+
<path d="m15 10-3 3-3-3" />
|
|
94
|
+
<rect width="20" height="14" x="2" y="3" rx="2" />
|
|
95
|
+
<path d="M12 17v4" />
|
|
96
|
+
<path d="M8 21h8" />
|
|
97
|
+
</svg>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function SmartphoneIcon(p: IconProps) {
|
|
102
|
+
return (
|
|
103
|
+
<svg {...svgProps(p)}>
|
|
104
|
+
<rect width="14" height="20" x="5" y="2" rx="2" />
|
|
105
|
+
<path d="M12 18h.01" />
|
|
106
|
+
</svg>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function XIcon(p: IconProps) {
|
|
111
|
+
return (
|
|
112
|
+
<svg {...svgProps(p)}>
|
|
113
|
+
<path d="M18 6 6 18" />
|
|
114
|
+
<path d="m6 6 12 12" />
|
|
115
|
+
</svg>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function CheckIcon(p: IconProps) {
|
|
120
|
+
return (
|
|
121
|
+
<svg {...svgProps(p)}>
|
|
122
|
+
<path d="M20 6 9 17l-5-5" />
|
|
123
|
+
</svg>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function LockIcon(p: IconProps) {
|
|
128
|
+
return (
|
|
129
|
+
<svg {...svgProps(p)}>
|
|
130
|
+
<rect width="18" height="11" x="3" y="11" rx="2" />
|
|
131
|
+
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
|
132
|
+
</svg>
|
|
133
|
+
)
|
|
134
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// @omg-dev/pwa — universal "Add to Home Screen" onboarding.
|
|
2
|
+
//
|
|
3
|
+
// Shared between the omg dashboard (workspace:*) and user-generated apps
|
|
4
|
+
// (registry + template bake). Self-contained styling — no Tailwind classes,
|
|
5
|
+
// no CSS imports; themes itself from the host's shadcn CSS variables with
|
|
6
|
+
// sane light/dark fallbacks.
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
detectPlatform,
|
|
10
|
+
isStandalone,
|
|
11
|
+
isInIframe,
|
|
12
|
+
promptNativeInstall,
|
|
13
|
+
isDismissed,
|
|
14
|
+
setDismissed,
|
|
15
|
+
countVisit,
|
|
16
|
+
type InstallPlatform,
|
|
17
|
+
type InstallState,
|
|
18
|
+
type BeforeInstallPromptEvent,
|
|
19
|
+
} from "./core"
|
|
20
|
+
|
|
21
|
+
export { useInstallPrompt, InstallGuide, VibesInstallPrompt, defaultAppName } from "./react"
|