@mayson-org/inject-script 1.0.7 → 1.0.9

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.
Files changed (48) hide show
  1. package/.env.example +4 -1
  2. package/README.md +33 -7
  3. package/assets/pwa/mayson-logo-dark-192x192.png +0 -0
  4. package/assets/pwa/mayson-logo-dark-512x512-maskable.png +0 -0
  5. package/assets/pwa/mayson-logo-dark-512x512.png +0 -0
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +18 -3
  8. package/dist/core/generators.d.ts.map +1 -1
  9. package/dist/core/generators.js +5 -4
  10. package/dist/core/remove-component.d.ts +8 -0
  11. package/dist/core/remove-component.d.ts.map +1 -0
  12. package/dist/core/remove-component.js +44 -0
  13. package/dist/core/types.d.ts +26 -6
  14. package/dist/core/types.d.ts.map +1 -1
  15. package/dist/index.d.ts +11 -3
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +102 -22
  18. package/dist/plugins/index.d.ts +2 -1
  19. package/dist/plugins/index.d.ts.map +1 -1
  20. package/dist/plugins/index.js +3 -1
  21. package/dist/plugins/pwa/index.d.ts +9 -0
  22. package/dist/plugins/pwa/index.d.ts.map +1 -0
  23. package/dist/plugins/pwa/index.js +11 -0
  24. package/dist/plugins/pwa/inject-project.d.ts +4 -0
  25. package/dist/plugins/pwa/inject-project.d.ts.map +1 -0
  26. package/dist/plugins/pwa/inject-project.js +169 -0
  27. package/dist/plugins/pwa/patch-layout-metadata.d.ts +14 -0
  28. package/dist/plugins/pwa/patch-layout-metadata.d.ts.map +1 -0
  29. package/dist/plugins/pwa/patch-layout-metadata.js +162 -0
  30. package/dist/plugins/pwa/templates/install-prompt.d.ts +3 -0
  31. package/dist/plugins/pwa/templates/install-prompt.d.ts.map +1 -0
  32. package/dist/plugins/pwa/templates/install-prompt.js +186 -0
  33. package/dist/plugins/pwa/templates/manifest.d.ts +11 -0
  34. package/dist/plugins/pwa/templates/manifest.d.ts.map +1 -0
  35. package/dist/plugins/pwa/templates/manifest.js +44 -0
  36. package/dist/plugins/pwa/templates/service-worker-register.d.ts +3 -0
  37. package/dist/plugins/pwa/templates/service-worker-register.d.ts.map +1 -0
  38. package/dist/plugins/pwa/templates/service-worker-register.js +23 -0
  39. package/dist/plugins/pwa/templates/sw.js.d.ts +6 -0
  40. package/dist/plugins/pwa/templates/sw.js.d.ts.map +1 -0
  41. package/dist/plugins/pwa/templates/sw.js.js +55 -0
  42. package/dist/shared/config.d.ts +2 -0
  43. package/dist/shared/config.d.ts.map +1 -1
  44. package/dist/shared/config.js +2 -0
  45. package/dist/shared/generated-app-metadata.d.ts +3 -2
  46. package/dist/shared/generated-app-metadata.d.ts.map +1 -1
  47. package/dist/shared/generated-app-metadata.js +24 -5
  48. package/package.json +4 -2
@@ -0,0 +1,169 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { injectComponentsIntoLayout } from '../../core/injector.js';
5
+ import { patchLayoutPwaMetadata } from './patch-layout-metadata.js';
6
+ import { buildManifestSource, MANIFEST_MARKER, } from './templates/manifest.js';
7
+ import { buildMaysonInstallPromptSource } from './templates/install-prompt.js';
8
+ import { buildMaysonServiceWorkerRegisterSource } from './templates/service-worker-register.js';
9
+ import { buildSwJsSource, SW_MARKER } from './templates/sw.js.js';
10
+ const DEFAULT_BRANDING = {
11
+ appName: 'My App',
12
+ themeColor: '#0a0a0a',
13
+ backgroundColor: '#ffffff',
14
+ description: 'Generated by Mayson',
15
+ };
16
+ const ICON_FILES = [
17
+ 'mayson-logo-dark-192x192.png',
18
+ 'mayson-logo-dark-512x512.png',
19
+ 'mayson-logo-dark-512x512-maskable.png',
20
+ ];
21
+ function resolvePwaAssetsDir() {
22
+ // dist/plugins/pwa → ../../../assets/pwa (package root)
23
+ const here = path.dirname(fileURLToPath(import.meta.url));
24
+ return path.resolve(here, '../../../assets/pwa');
25
+ }
26
+ function resolveBranding(ctx) {
27
+ const pwa = ctx.context?.pwa;
28
+ return {
29
+ appName: pwa?.appName?.trim() || DEFAULT_BRANDING.appName,
30
+ themeColor: pwa?.themeColor?.trim() || DEFAULT_BRANDING.themeColor,
31
+ backgroundColor: pwa?.backgroundColor?.trim() || DEFAULT_BRANDING.backgroundColor,
32
+ description: pwa?.description?.trim() || DEFAULT_BRANDING.description,
33
+ };
34
+ }
35
+ function writeMarkedFile(params) {
36
+ const { filePath, source, marker, dryRun, label } = params;
37
+ if (fs.existsSync(filePath)) {
38
+ const existing = fs.readFileSync(filePath, 'utf-8');
39
+ if (!existing.includes(marker)) {
40
+ return `skipped ${label} — already exists without ${marker} marker`;
41
+ }
42
+ if (dryRun) {
43
+ return `would refresh ${label}`;
44
+ }
45
+ fs.writeFileSync(filePath, source, 'utf-8');
46
+ return `refreshed ${label}`;
47
+ }
48
+ if (dryRun) {
49
+ return `would write ${label}`;
50
+ }
51
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
52
+ fs.writeFileSync(filePath, source, 'utf-8');
53
+ return `wrote ${label}`;
54
+ }
55
+ function copyDefaultIcons(projectRoot, dryRun) {
56
+ const messages = [];
57
+ const assetsDir = resolvePwaAssetsDir();
58
+ const iconsDir = path.join(projectRoot, 'public', 'icons');
59
+ for (const file of ICON_FILES) {
60
+ const dest = path.join(iconsDir, file);
61
+ const src = path.join(assetsDir, file);
62
+ if (fs.existsSync(dest)) {
63
+ messages.push(`skipped icon ${file} — already present`);
64
+ continue;
65
+ }
66
+ if (!fs.existsSync(src)) {
67
+ messages.push(`missing packaged icon ${file} at ${src}`);
68
+ continue;
69
+ }
70
+ if (dryRun) {
71
+ messages.push(`would copy icon ${file}`);
72
+ continue;
73
+ }
74
+ fs.mkdirSync(iconsDir, { recursive: true });
75
+ fs.copyFileSync(src, dest);
76
+ messages.push(`copied icon ${file}`);
77
+ }
78
+ return messages;
79
+ }
80
+ function writePwaComponents(ctx) {
81
+ const { layoutPath, dryRun = false, isTypeScript = true } = ctx;
82
+ const layoutDir = path.dirname(layoutPath);
83
+ const ext = isTypeScript ? '.tsx' : '.jsx';
84
+ const messages = [];
85
+ const components = [];
86
+ const files = [
87
+ {
88
+ name: 'MaysonServiceWorkerRegister',
89
+ source: buildMaysonServiceWorkerRegisterSource(),
90
+ },
91
+ {
92
+ name: 'MaysonInstallPrompt',
93
+ source: buildMaysonInstallPromptSource(),
94
+ },
95
+ ];
96
+ for (const file of files) {
97
+ const filePath = path.join(layoutDir, `${file.name}${ext}`);
98
+ if (!dryRun) {
99
+ fs.writeFileSync(filePath, file.source, 'utf-8');
100
+ }
101
+ messages.push(dryRun ? `would write ${file.name}${ext}` : `wrote ${file.name}${ext}`);
102
+ components.push({
103
+ name: file.name,
104
+ filePath,
105
+ importStatement: `import { ${file.name} } from './${file.name}';`,
106
+ jsxTag: `<${file.name} />`,
107
+ });
108
+ }
109
+ return { components, messages };
110
+ }
111
+ /** Orchestrate all PWA project-level writes + layout inject for SW/install components. */
112
+ export function injectPwaProject(ctx) {
113
+ const messages = [];
114
+ const injectedComponents = [];
115
+ const skippedComponents = [];
116
+ const { projectRoot, layoutPath, dryRun = false } = ctx;
117
+ const branding = resolveBranding(ctx);
118
+ const appDir = path.dirname(layoutPath);
119
+ // 1. Components next to layout + inject into layout
120
+ const { components, messages: compMessages } = writePwaComponents(ctx);
121
+ messages.push(...compMessages);
122
+ const injectResult = injectComponentsIntoLayout({
123
+ filePath: layoutPath,
124
+ components,
125
+ dryRun,
126
+ });
127
+ injectedComponents.push(...injectResult.injectedComponents);
128
+ skippedComponents.push(...injectResult.skippedComponents);
129
+ if (injectResult.error) {
130
+ messages.push(`layout inject error: ${injectResult.error}`);
131
+ }
132
+ // 2. app/manifest.ts
133
+ messages.push(writeMarkedFile({
134
+ filePath: path.join(appDir, 'manifest.ts'),
135
+ source: buildManifestSource(branding),
136
+ marker: MANIFEST_MARKER,
137
+ dryRun,
138
+ label: 'app/manifest.ts',
139
+ }));
140
+ // 3. public/sw.js
141
+ messages.push(writeMarkedFile({
142
+ filePath: path.join(projectRoot, 'public', 'sw.js'),
143
+ source: buildSwJsSource(),
144
+ marker: SW_MARKER,
145
+ dryRun,
146
+ label: 'public/sw.js',
147
+ }));
148
+ // 4. Icons (never overwrite)
149
+ messages.push(...copyDefaultIcons(projectRoot, dryRun));
150
+ // 5. Patch layout metadata (appleWebApp + themeColor)
151
+ if (fs.existsSync(layoutPath)) {
152
+ // Re-read after component inject may have written the file
153
+ const layoutContent = dryRun
154
+ ? fs.readFileSync(layoutPath, 'utf-8')
155
+ : fs.readFileSync(layoutPath, 'utf-8');
156
+ const patched = patchLayoutPwaMetadata(layoutContent, {
157
+ appName: branding.appName,
158
+ themeColor: branding.themeColor,
159
+ });
160
+ messages.push(...patched.messages);
161
+ if (patched.changed && !dryRun) {
162
+ fs.writeFileSync(layoutPath, patched.content, 'utf-8');
163
+ }
164
+ else if (patched.changed && dryRun) {
165
+ messages.push('would patch layout PWA metadata');
166
+ }
167
+ }
168
+ return { messages, injectedComponents, skippedComponents };
169
+ }
@@ -0,0 +1,14 @@
1
+ export type PwaLayoutBranding = {
2
+ appName: string;
3
+ themeColor: string;
4
+ };
5
+ /**
6
+ * Insert appleWebApp / viewport.themeColor when absent.
7
+ * Does not rewrite existing custom appleWebApp or themeColor values.
8
+ */
9
+ export declare function patchLayoutPwaMetadata(content: string, branding: PwaLayoutBranding): {
10
+ content: string;
11
+ changed: boolean;
12
+ messages: string[];
13
+ };
14
+ //# sourceMappingURL=patch-layout-metadata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patch-layout-metadata.d.ts","sourceRoot":"","sources":["../../../src/plugins/pwa/patch-layout-metadata.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,iBAAiB,GAC1B;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CAkC3D"}
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Insert appleWebApp / viewport.themeColor when absent.
3
+ * Does not rewrite existing custom appleWebApp or themeColor values.
4
+ */
5
+ export function patchLayoutPwaMetadata(content, branding) {
6
+ let next = content;
7
+ const messages = [];
8
+ let changed = false;
9
+ if (!/\bappleWebApp\s*:/.test(next)) {
10
+ const patched = insertAppleWebApp(next, branding.appName);
11
+ if (patched !== next) {
12
+ next = patched;
13
+ changed = true;
14
+ messages.push('added appleWebApp to layout metadata');
15
+ }
16
+ }
17
+ else {
18
+ messages.push('layout already has appleWebApp — left unchanged');
19
+ }
20
+ if (!/\bthemeColor\s*:/.test(next)) {
21
+ const patched = insertThemeColor(next, branding.themeColor);
22
+ if (patched !== next) {
23
+ next = patched;
24
+ changed = true;
25
+ messages.push('added viewport.themeColor to layout');
26
+ }
27
+ }
28
+ else {
29
+ messages.push('layout already has themeColor — left unchanged');
30
+ }
31
+ // Ensure Viewport type import when we added/need viewport export
32
+ if (/\bexport\s+const\s+viewport\b/.test(next) && !/\bViewport\b/.test(next)) {
33
+ next = ensureViewportImport(next);
34
+ changed = true;
35
+ }
36
+ return { content: next, changed, messages };
37
+ }
38
+ function insertAppleWebApp(content, appName) {
39
+ const appleBlock = ` appleWebApp: {
40
+ capable: true,
41
+ title: ${JSON.stringify(appName)},
42
+ statusBarStyle: 'default',
43
+ },`;
44
+ // Prefer inserting inside an existing `export const metadata = { ... }` object
45
+ const metadataMatch = content.match(/export\s+const\s+metadata(?:\s*:\s*[^=]+)?\s*=\s*\{/);
46
+ if (metadataMatch && metadataMatch.index !== undefined) {
47
+ const openBrace = metadataMatch.index + metadataMatch[0].length - 1;
48
+ const closeBrace = findMatchingBrace(content, openBrace);
49
+ if (closeBrace !== -1) {
50
+ const before = content.slice(0, closeBrace);
51
+ const needsComma = /,\s*$/.test(before.trimEnd()) ? '' : ',';
52
+ return (before.trimEnd() +
53
+ needsComma +
54
+ '\n' +
55
+ appleBlock +
56
+ '\n' +
57
+ content.slice(closeBrace));
58
+ }
59
+ }
60
+ // No metadata export — prepend a minimal one
61
+ const importEnd = findLastImportEnd(content);
62
+ const block = `export const metadata = {
63
+ ${appleBlock}
64
+ };
65
+
66
+ `;
67
+ if (importEnd === 0) {
68
+ return block + content;
69
+ }
70
+ return content.slice(0, importEnd) + '\n\n' + block + content.slice(importEnd);
71
+ }
72
+ function insertThemeColor(content, themeColor) {
73
+ if (/\bexport\s+const\s+viewport\b/.test(content)) {
74
+ const viewportMatch = content.match(/export\s+const\s+viewport(?:\s*:\s*[^=]+)?\s*=\s*\{/);
75
+ if (viewportMatch && viewportMatch.index !== undefined) {
76
+ const openBrace = viewportMatch.index + viewportMatch[0].length - 1;
77
+ const closeBrace = findMatchingBrace(content, openBrace);
78
+ if (closeBrace !== -1) {
79
+ const before = content.slice(0, closeBrace);
80
+ const needsComma = /,\s*$/.test(before.trimEnd()) ? '' : ',';
81
+ const line = ` themeColor: ${JSON.stringify(themeColor)},`;
82
+ return (before.trimEnd() +
83
+ needsComma +
84
+ '\n' +
85
+ line +
86
+ '\n' +
87
+ content.slice(closeBrace));
88
+ }
89
+ }
90
+ return content;
91
+ }
92
+ const block = `export const viewport = {
93
+ themeColor: ${JSON.stringify(themeColor)},
94
+ };
95
+
96
+ `;
97
+ // Place after metadata export if present
98
+ const metadataEnd = findExportObjectEnd(content, 'metadata');
99
+ if (metadataEnd !== -1) {
100
+ return content.slice(0, metadataEnd) + '\n\n' + block + content.slice(metadataEnd);
101
+ }
102
+ const importEnd = findLastImportEnd(content);
103
+ if (importEnd === 0) {
104
+ return block + content;
105
+ }
106
+ return content.slice(0, importEnd) + '\n\n' + block + content.slice(importEnd);
107
+ }
108
+ function ensureViewportImport(content) {
109
+ const fromNext = content.match(/import\s+type\s*\{([^}]*)\}\s*from\s*['"]next['"]/);
110
+ if (fromNext) {
111
+ const names = fromNext[1];
112
+ if (/\bViewport\b/.test(names)) {
113
+ return content;
114
+ }
115
+ const updated = names.trim().length
116
+ ? `${names.trim()}, Viewport`
117
+ : 'Viewport';
118
+ return content.replace(fromNext[0], `import type { ${updated} } from 'next'`);
119
+ }
120
+ // Combined value+type import: import { type Metadata, type Viewport } from 'next'
121
+ const mixed = content.match(/import\s*\{([^}]*)\}\s*from\s*['"]next['"]/);
122
+ if (mixed && /\bMetadata\b/.test(mixed[1]) && !/\bViewport\b/.test(mixed[1])) {
123
+ return content.replace(mixed[0], `import {${mixed[1]}, type Viewport} from 'next'`);
124
+ }
125
+ return `import type { Viewport } from 'next';\n` + content;
126
+ }
127
+ function findMatchingBrace(source, openIndex) {
128
+ let depth = 0;
129
+ for (let i = openIndex; i < source.length; i++) {
130
+ const ch = source[i];
131
+ if (ch === '{')
132
+ depth++;
133
+ else if (ch === '}') {
134
+ depth--;
135
+ if (depth === 0)
136
+ return i;
137
+ }
138
+ }
139
+ return -1;
140
+ }
141
+ function findLastImportEnd(content) {
142
+ const matches = Array.from(content.matchAll(/^import\s.+?;$/gm));
143
+ if (matches.length === 0)
144
+ return 0;
145
+ const last = matches[matches.length - 1];
146
+ return (last.index ?? 0) + last[0].length;
147
+ }
148
+ function findExportObjectEnd(content, name) {
149
+ const re = new RegExp(`export\\s+const\\s+${name}(?:\\s*:\\s*[^=]+)?\\s*=\\s*\\{`);
150
+ const match = content.match(re);
151
+ if (!match || match.index === undefined)
152
+ return -1;
153
+ const openBrace = match.index + match[0].length - 1;
154
+ const close = findMatchingBrace(content, openBrace);
155
+ if (close === -1)
156
+ return -1;
157
+ // Include trailing semicolon if present
158
+ let end = close + 1;
159
+ if (content[end] === ';')
160
+ end++;
161
+ return end;
162
+ }
@@ -0,0 +1,3 @@
1
+ /** Source for MaysonInstallPrompt — inline styles, host-shell ?pwa_install=1 contract. */
2
+ export declare function buildMaysonInstallPromptSource(): string;
3
+ //# sourceMappingURL=install-prompt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install-prompt.d.ts","sourceRoot":"","sources":["../../../../src/plugins/pwa/templates/install-prompt.ts"],"names":[],"mappings":"AAAA,0FAA0F;AAC1F,wBAAgB,8BAA8B,IAAI,MAAM,CAwLvD"}
@@ -0,0 +1,186 @@
1
+ /** Source for MaysonInstallPrompt — inline styles, host-shell ?pwa_install=1 contract. */
2
+ export function buildMaysonInstallPromptSource() {
3
+ return `'use client';
4
+
5
+ import { useEffect, useState, type CSSProperties } from 'react';
6
+
7
+ const DISMISS_KEY = 'mayson-pwa-install-dismissed';
8
+
9
+ interface BeforeInstallPromptEvent extends Event {
10
+ prompt: () => Promise<void>;
11
+ userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
12
+ }
13
+
14
+ function isIOS(): boolean {
15
+ if (typeof navigator === 'undefined') return false;
16
+ return /iphone|ipad|ipod/i.test(navigator.userAgent);
17
+ }
18
+
19
+ function isStandalone(): boolean {
20
+ if (typeof window === 'undefined') return false;
21
+ return (
22
+ window.matchMedia('(display-mode: standalone)').matches ||
23
+ (navigator as Navigator & { standalone?: boolean }).standalone === true
24
+ );
25
+ }
26
+
27
+ export function MaysonInstallPrompt() {
28
+ const [deferred, setDeferred] = useState<BeforeInstallPromptEvent | null>(null);
29
+ const [visible, setVisible] = useState(false);
30
+ const [showIOSHint, setShowIOSHint] = useState(false);
31
+ const [showBrowserHint, setShowBrowserHint] = useState(false);
32
+
33
+ useEffect(() => {
34
+ const params = new URLSearchParams(window.location.search);
35
+ const forced = params.get('pwa_install') === '1';
36
+ if (forced) {
37
+ params.delete('pwa_install');
38
+ const qs = params.toString();
39
+ history.replaceState(
40
+ null,
41
+ '',
42
+ window.location.pathname + (qs ? \`?\${qs}\` : '') + window.location.hash,
43
+ );
44
+ setVisible(true);
45
+ } else if (isStandalone() || localStorage.getItem(DISMISS_KEY) === '1') {
46
+ return;
47
+ }
48
+
49
+ const onPrompt = (e: Event) => {
50
+ e.preventDefault();
51
+ setDeferred(e as BeforeInstallPromptEvent);
52
+ setVisible(true);
53
+ };
54
+ window.addEventListener('beforeinstallprompt', onPrompt);
55
+
56
+ if (isIOS()) setVisible(true);
57
+
58
+ return () => window.removeEventListener('beforeinstallprompt', onPrompt);
59
+ }, []);
60
+
61
+ if (!visible) return null;
62
+
63
+ const dismiss = () => {
64
+ localStorage.setItem(DISMISS_KEY, '1');
65
+ setVisible(false);
66
+ };
67
+
68
+ const install = async () => {
69
+ if (deferred) {
70
+ await deferred.prompt();
71
+ const { outcome } = await deferred.userChoice;
72
+ if (outcome === 'accepted') setVisible(false);
73
+ setDeferred(null);
74
+ } else if (isIOS()) {
75
+ setShowIOSHint(true);
76
+ } else {
77
+ setShowBrowserHint(true);
78
+ }
79
+ };
80
+
81
+ const showDismiss = !showIOSHint && !showBrowserHint;
82
+ const cornerMask = showDismiss
83
+ ? 'radial-gradient(circle 20px at 100% 0, transparent 16px, #000 17px)'
84
+ : undefined;
85
+
86
+ const shellStyle: CSSProperties = {
87
+ position: 'fixed',
88
+ bottom: 16,
89
+ left: '50%',
90
+ zIndex: 50,
91
+ width: 'calc(100% - 2rem)',
92
+ maxWidth: 384,
93
+ transform: 'translateX(-50%)',
94
+ };
95
+
96
+ const cardStyle: CSSProperties = {
97
+ borderRadius: 24,
98
+ background: '#171718',
99
+ border: '0.6px solid #46454866',
100
+ boxShadow: '0px 8px 12px 6px #00000026',
101
+ padding: '12px 20px',
102
+ color: '#9D9DA2',
103
+ fontFamily: '"Google Sans", system-ui, -apple-system, sans-serif',
104
+ fontSize: 14,
105
+ lineHeight: 1.4,
106
+ ...(cornerMask
107
+ ? { maskImage: cornerMask, WebkitMaskImage: cornerMask }
108
+ : {}),
109
+ };
110
+
111
+ const installBtnStyle: CSSProperties = {
112
+ cursor: 'pointer',
113
+ borderRadius: 8,
114
+ background: '#fafafa',
115
+ color: '#171718',
116
+ border: 'none',
117
+ padding: '6px 12px',
118
+ fontSize: 14,
119
+ fontWeight: 500,
120
+ fontFamily: 'inherit',
121
+ flexShrink: 0,
122
+ };
123
+
124
+ const dismissBtnStyle: CSSProperties = {
125
+ position: 'absolute',
126
+ top: -6,
127
+ right: -6,
128
+ zIndex: 10,
129
+ display: 'flex',
130
+ alignItems: 'center',
131
+ justifyContent: 'center',
132
+ width: 20,
133
+ height: 20,
134
+ cursor: 'pointer',
135
+ fontSize: 12,
136
+ color: '#737373',
137
+ background: '#171718',
138
+ border: '0.6px solid #46454866',
139
+ borderRadius: '9999px',
140
+ padding: 0,
141
+ };
142
+
143
+ return (
144
+ <div style={shellStyle}>
145
+ <div style={cardStyle}>
146
+ {showIOSHint ? (
147
+ <p style={{ margin: 0 }}>
148
+ To install: tap the &quot;Share&quot; button in Safari, then choose &quot;Add to
149
+ Home Screen&quot;.
150
+ </p>
151
+ ) : showBrowserHint ? (
152
+ <p style={{ margin: 0 }}>
153
+ To install: open your browser menu (⋮) and choose &quot;Install app&quot; or
154
+ &quot;Add to Home Screen&quot;
155
+ </p>
156
+ ) : (
157
+ <div
158
+ style={{
159
+ display: 'flex',
160
+ alignItems: 'center',
161
+ justifyContent: 'space-between',
162
+ gap: 12,
163
+ }}
164
+ >
165
+ <p style={{ margin: 0 }}>Install this app on your device</p>
166
+ <button type="button" onClick={install} style={installBtnStyle}>
167
+ Install
168
+ </button>
169
+ </div>
170
+ )}
171
+ </div>
172
+ {showDismiss && (
173
+ <button
174
+ type="button"
175
+ onClick={dismiss}
176
+ aria-label="Dismiss install prompt"
177
+ style={dismissBtnStyle}
178
+ >
179
+
180
+ </button>
181
+ )}
182
+ </div>
183
+ );
184
+ }
185
+ `;
186
+ }
@@ -0,0 +1,11 @@
1
+ export type ManifestBranding = {
2
+ appName: string;
3
+ themeColor: string;
4
+ backgroundColor: string;
5
+ description: string;
6
+ };
7
+ declare const MANIFEST_MARKER = "@mayson-pwa-manifest";
8
+ /** Next.js app/manifest.ts source with Mayson ownership marker. */
9
+ export declare function buildManifestSource(branding: ManifestBranding): string;
10
+ export { MANIFEST_MARKER };
11
+ //# sourceMappingURL=manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../../../src/plugins/pwa/templates/manifest.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,QAAA,MAAM,eAAe,yBAAyB,CAAC;AAE/C,mEAAmE;AACnE,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,gBAAgB,GAAG,MAAM,CAyCtE;AAED,OAAO,EAAE,eAAe,EAAE,CAAC"}
@@ -0,0 +1,44 @@
1
+ const MANIFEST_MARKER = '@mayson-pwa-manifest';
2
+ /** Next.js app/manifest.ts source with Mayson ownership marker. */
3
+ export function buildManifestSource(branding) {
4
+ const name = JSON.stringify(branding.appName);
5
+ const description = JSON.stringify(branding.description);
6
+ const theme = JSON.stringify(branding.themeColor);
7
+ const bg = JSON.stringify(branding.backgroundColor);
8
+ return `import type { MetadataRoute } from 'next';
9
+
10
+ // ${MANIFEST_MARKER}
11
+ // PWA manifest — served at /manifest.webmanifest.
12
+ export default function manifest(): MetadataRoute.Manifest {
13
+ return {
14
+ name: ${name},
15
+ short_name: ${name},
16
+ description: ${description},
17
+ start_url: '/',
18
+ scope: '/',
19
+ display: 'standalone',
20
+ background_color: ${bg},
21
+ theme_color: ${theme},
22
+ icons: [
23
+ {
24
+ src: '/icons/mayson-logo-dark-192x192.png',
25
+ sizes: '192x192',
26
+ type: 'image/png',
27
+ },
28
+ {
29
+ src: '/icons/mayson-logo-dark-512x512.png',
30
+ sizes: '512x512',
31
+ type: 'image/png',
32
+ },
33
+ {
34
+ src: '/icons/mayson-logo-dark-512x512-maskable.png',
35
+ sizes: '512x512',
36
+ type: 'image/png',
37
+ purpose: 'maskable',
38
+ },
39
+ ],
40
+ };
41
+ }
42
+ `;
43
+ }
44
+ export { MANIFEST_MARKER };
@@ -0,0 +1,3 @@
1
+ /** Source for MaysonServiceWorkerRegister — production-only SW registration. */
2
+ export declare function buildMaysonServiceWorkerRegisterSource(): string;
3
+ //# sourceMappingURL=service-worker-register.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-worker-register.d.ts","sourceRoot":"","sources":["../../../../src/plugins/pwa/templates/service-worker-register.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,wBAAgB,sCAAsC,IAAI,MAAM,CAqB/D"}
@@ -0,0 +1,23 @@
1
+ /** Source for MaysonServiceWorkerRegister — production-only SW registration. */
2
+ export function buildMaysonServiceWorkerRegisterSource() {
3
+ return `'use client';
4
+
5
+ import { useEffect } from 'react';
6
+
7
+ /** Registers the PWA service worker. Production-only — avoids caching stale HMR chunks in dev. */
8
+ export function MaysonServiceWorkerRegister() {
9
+ useEffect(() => {
10
+ if (process.env.NODE_ENV !== 'production') return;
11
+ if (!('serviceWorker' in navigator)) return;
12
+ const onLoad = () => {
13
+ navigator.serviceWorker.register('/sw.js').catch(() => {
14
+ // Installability is a nice-to-have — never break the app over it.
15
+ });
16
+ };
17
+ window.addEventListener('load', onLoad);
18
+ return () => window.removeEventListener('load', onLoad);
19
+ }, []);
20
+ return null;
21
+ }
22
+ `;
23
+ }
@@ -0,0 +1,6 @@
1
+ declare const SW_MARKER = "@mayson-pwa-sw";
2
+ declare const CACHE_VERSION = "mayson-pwa-v1";
3
+ /** public/sw.js source with Mayson ownership marker. */
4
+ export declare function buildSwJsSource(): string;
5
+ export { SW_MARKER, CACHE_VERSION };
6
+ //# sourceMappingURL=sw.js.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sw.js.d.ts","sourceRoot":"","sources":["../../../../src/plugins/pwa/templates/sw.js.ts"],"names":[],"mappings":"AAAA,QAAA,MAAM,SAAS,mBAAmB,CAAC;AACnC,QAAA,MAAM,aAAa,kBAAkB,CAAC;AAEtC,wDAAwD;AACxD,wBAAgB,eAAe,IAAI,MAAM,CAkDxC;AAED,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,CAAC"}