offline-page-kit 0.3.2 → 0.3.3

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/README.md CHANGED
@@ -77,6 +77,18 @@ Just reliable navigation fallback.
77
77
 
78
78
  ---
79
79
 
80
+ # 🟣 Demo
81
+
82
+ <p align="center">
83
+ <img src="./img/example.png" width="600" alt="demo gif placeholder">
84
+ </p>
85
+
86
+ > - Online page load
87
+ > - DevTools → Offline mode
88
+ > - Refresh → Offline page appears
89
+
90
+ ---
91
+
80
92
  # 🚀 Quick Start
81
93
 
82
94
  ### 1️⃣ Install
@@ -157,43 +169,31 @@ Install → Activate → Control → Fetch
157
169
 
158
170
  ```tsx
159
171
 
160
- // #/utils/SWRegister.tsx
172
+ // #/app/page.tsx
161
173
  "use client";
162
174
 
163
175
  import { useEffect } from "react";
164
176
  import { registerOfflineKit } from "offline-page-kit";
165
177
 
166
- export default function SWRegister() {
178
+ export default function Home() {
167
179
  useEffect(() => {
168
- registerOfflineKit({ debug: true });
180
+ registerOfflineKit(
181
+ {
182
+ swUrl: "/sw.js",
183
+ scope: "/",
184
+ debug: true
185
+ }
186
+ );
169
187
  }, []);
170
- return null;
171
- }
172
-
173
-
174
-
175
- // Root layout
176
- import SWRegister from "@/utils/SWRegister";
177
-
178
- // ...
179
-
180
- export default function RootLayout({
181
- children,
182
- }: Readonly<{
183
- children: React.ReactNode;
184
- }>) {
185
188
  return (
186
- <html lang="en">
187
- <body
188
- className={`${geistSans.variable} ${geistMono.variable} antialiased`}
189
- >
190
- {children}
191
- <SWRegister />
192
- </body>
193
- </html>
189
+ <main className="flex min-h-screen flex-col items-center justify-between p-24">
190
+ <h1 className="text-4xl font-bold">You are Online.</h1>
191
+ </main>
194
192
  );
195
193
  }
196
194
 
195
+
196
+
197
197
  ```
198
198
 
199
199
  ---
package/dist/cli.cjs CHANGED
@@ -73,26 +73,403 @@ function offlineHtmlTemplate(title = "You're Offline") {
73
73
  <meta charset="UTF-8"/>
74
74
  <meta name="viewport" content="width=device-width,initial-scale=1"/>
75
75
  <title>${title}</title>
76
+ <meta name="theme-color" content="#0b0f19"/>
77
+
76
78
  <style>
77
- body{margin:0;height:100vh;display:grid;place-items:center;font-family:system-ui,-apple-system,Segoe UI,Roboto;background:#0b0f19;color:#fff}
78
- .box{max-width:560px;padding:28px 26px;border-radius:18px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.04)}
79
- h1{margin:0 0 10px;font-size:28px}
80
- p{margin:0 0 18px;opacity:.85;line-height:1.5}
81
- .row{display:flex;gap:10px;flex-wrap:wrap}
82
- button,a{appearance:none;border:0;border-radius:12px;padding:10px 14px;cursor:pointer;text-decoration:none}
83
- button{background:#fff;color:#111}
84
- a{background:rgba(255,255,255,.1);color:#fff}
79
+ :root{
80
+ --bg0:#060912;
81
+ --bg1:#0b1022;
82
+ --card: rgba(255,255,255,.06);
83
+ --card2: rgba(255,255,255,.04);
84
+ --stroke: rgba(255,255,255,.14);
85
+ --text:#ffffff;
86
+ --muted: rgba(255,255,255,.78);
87
+ --muted2: rgba(255,255,255,.62);
88
+ --good:#2ee59d;
89
+ --warn:#ffd166;
90
+ --shadow: 0 20px 80px rgba(0,0,0,.55);
91
+ --radius: 22px;
92
+ }
93
+
94
+ *{box-sizing:border-box}
95
+ html,body{height:100%}
96
+ body{
97
+ margin:0;
98
+ font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji";
99
+ color:var(--text);
100
+ background:
101
+ radial-gradient(900px 500px at 18% 18%, rgba(120,140,255,.20), transparent 60%),
102
+ radial-gradient(700px 500px at 82% 20%, rgba(46,229,157,.16), transparent 58%),
103
+ radial-gradient(600px 520px at 50% 95%, rgba(255,209,102,.14), transparent 55%),
104
+ linear-gradient(180deg, var(--bg0), var(--bg1));
105
+ overflow:hidden;
106
+ }
107
+
108
+ /* Floating blobs (subtle animation) */
109
+ .blob{
110
+ position:absolute; inset:auto;
111
+ width: 520px; height: 520px;
112
+ border-radius: 50%;
113
+ filter: blur(42px);
114
+ opacity:.35;
115
+ animation: float 10s ease-in-out infinite;
116
+ pointer-events:none;
117
+ transform: translate3d(0,0,0);
118
+ }
119
+ .blob.b1{left:-180px; top:-200px; background: rgba(120,140,255,.55); animation-duration: 12s;}
120
+ .blob.b2{right:-210px; top:-150px; background: rgba(46,229,157,.52); animation-duration: 14s;}
121
+ .blob.b3{left:18%; bottom:-320px; background: rgba(255,209,102,.45); animation-duration: 16s;}
122
+
123
+ @keyframes float{
124
+ 0%,100%{ transform: translate(0,0) scale(1);}
125
+ 50%{ transform: translate(22px,-18px) scale(1.06);}
126
+ }
127
+
128
+ /* Main layout */
129
+ .wrap{
130
+ min-height:100%;
131
+ display:grid;
132
+ place-items:center;
133
+ padding: 24px;
134
+ position:relative;
135
+ z-index:1;
136
+ }
137
+
138
+ .card{
139
+ width:min(720px, 100%);
140
+ border-radius: var(--radius);
141
+ background: linear-gradient(180deg, var(--card), var(--card2));
142
+ border:1px solid var(--stroke);
143
+ box-shadow: var(--shadow);
144
+ padding: 26px;
145
+ backdrop-filter: blur(10px);
146
+ -webkit-backdrop-filter: blur(10px);
147
+
148
+ animation: pop .55s cubic-bezier(.2,.9,.2,1) both;
149
+ }
150
+
151
+ @keyframes pop{
152
+ from{ opacity:0; transform: translateY(14px) scale(.98); }
153
+ to{ opacity:1; transform: translateY(0) scale(1); }
154
+ }
155
+
156
+ .top{
157
+ display:flex;
158
+ align-items:flex-start;
159
+ justify-content:space-between;
160
+ gap:16px;
161
+ margin-bottom: 14px;
162
+ }
163
+
164
+ .badge{
165
+ display:inline-flex;
166
+ align-items:center;
167
+ gap:10px;
168
+ padding: 10px 12px;
169
+ border-radius: 999px;
170
+ border:1px solid rgba(255,255,255,.14);
171
+ background: rgba(0,0,0,.18);
172
+ color: var(--muted);
173
+ font-size: 13px;
174
+ user-select:none;
175
+ white-space:nowrap;
176
+ }
177
+
178
+ /* Ping dot */
179
+ .dot{
180
+ width:10px;height:10px;border-radius:50%;
181
+ background: var(--warn);
182
+ box-shadow: 0 0 0 0 rgba(255,209,102,.65);
183
+ animation: ping 1.6s infinite;
184
+ }
185
+ @keyframes ping{
186
+ 0%{ box-shadow: 0 0 0 0 rgba(255,209,102,.55); }
187
+ 70%{ box-shadow: 0 0 0 10px rgba(255,209,102,0); }
188
+ 100%{ box-shadow: 0 0 0 0 rgba(255,209,102,0); }
189
+ }
190
+
191
+ h1{
192
+ margin: 0 0 8px;
193
+ font-size: clamp(26px, 3.3vw, 34px);
194
+ letter-spacing: -0.02em;
195
+ line-height: 1.15;
196
+ }
197
+
198
+ p{
199
+ margin: 0;
200
+ color: var(--muted2);
201
+ line-height: 1.55;
202
+ font-size: 15.5px;
203
+ }
204
+
205
+ .hero{
206
+ display:flex;
207
+ align-items:center;
208
+ gap: 14px;
209
+ margin-top: 10px;
210
+ }
211
+
212
+ /* Animated wifi icon */
213
+ .wifi{
214
+ width: 54px; height: 54px;
215
+ border-radius: 16px;
216
+ display:grid; place-items:center;
217
+ background: rgba(255,255,255,.06);
218
+ border:1px solid rgba(255,255,255,.12);
219
+ position:relative;
220
+ overflow:hidden;
221
+ }
222
+ .wifi::after{
223
+ content:"";
224
+ position:absolute; inset:-40%;
225
+ background: radial-gradient(circle at 30% 30%, rgba(255,255,255,.18), transparent 55%);
226
+ animation: sheen 2.8s ease-in-out infinite;
227
+ }
228
+ @keyframes sheen{
229
+ 0%,100%{ transform: translate(-8px,-6px) rotate(0deg); opacity:.75;}
230
+ 50%{ transform: translate(10px,8px) rotate(8deg); opacity:.9;}
231
+ }
232
+
233
+ .wifi svg{ position:relative; z-index:1; opacity:.95; }
234
+ .wifi path{ animation: wave 1.6s ease-in-out infinite; transform-origin:center; }
235
+ .wifi path:nth-child(1){ opacity:.35; animation-delay:.05s;}
236
+ .wifi path:nth-child(2){ opacity:.55; animation-delay:.12s;}
237
+ .wifi path:nth-child(3){ opacity:.75; animation-delay:.2s;}
238
+ .wifi circle{ opacity:.9; animation: blink 1.2s ease-in-out infinite; }
239
+
240
+ @keyframes wave{
241
+ 0%,100%{ transform: scale(1); }
242
+ 50%{ transform: scale(1.06); }
243
+ }
244
+ @keyframes blink{
245
+ 0%,100%{ opacity:.9; }
246
+ 50%{ opacity:.45; }
247
+ }
248
+
249
+ .actions{
250
+ display:flex;
251
+ flex-wrap:wrap;
252
+ gap: 10px;
253
+ margin-top: 18px;
254
+ }
255
+
256
+ .btn{
257
+ appearance:none;
258
+ border:0;
259
+ border-radius: 14px;
260
+ padding: 11px 14px;
261
+ cursor:pointer;
262
+ text-decoration:none;
263
+ display:inline-flex;
264
+ align-items:center;
265
+ gap:10px;
266
+ font-weight: 650;
267
+ font-size: 14.5px;
268
+ letter-spacing: .01em;
269
+ transition: transform .12s ease, filter .12s ease, background .2s ease, border-color .2s ease;
270
+ user-select:none;
271
+ }
272
+ .btn:active{ transform: translateY(1px) scale(.99); }
273
+
274
+ .primary{
275
+ background: #ffffff;
276
+ color: #0b0f19;
277
+ box-shadow: 0 8px 24px rgba(255,255,255,.12);
278
+ }
279
+ .primary:hover{ filter: brightness(0.98); }
280
+
281
+ .ghost{
282
+ background: rgba(255,255,255,.08);
283
+ color: var(--text);
284
+ border: 1px solid rgba(255,255,255,.14);
285
+ }
286
+ .ghost:hover{
287
+ background: rgba(255,255,255,.10);
288
+ border-color: rgba(255,255,255,.18);
289
+ }
290
+
291
+ .hint{
292
+ margin-top: 14px;
293
+ display:flex;
294
+ gap:10px;
295
+ flex-wrap:wrap;
296
+ align-items:center;
297
+ color: var(--muted2);
298
+ font-size: 13px;
299
+ }
300
+
301
+ .kbd{
302
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
303
+ font-size: 12px;
304
+ padding: 4px 8px;
305
+ border-radius: 9px;
306
+ border:1px solid rgba(255,255,255,.14);
307
+ background: rgba(0,0,0,.20);
308
+ color: rgba(255,255,255,.82);
309
+ }
310
+
311
+ /* Toast */
312
+ .toast{
313
+ position: fixed;
314
+ left: 50%;
315
+ bottom: 18px;
316
+ transform: translateX(-50%);
317
+ padding: 10px 12px;
318
+ border-radius: 999px;
319
+ background: rgba(0,0,0,.40);
320
+ border:1px solid rgba(255,255,255,.14);
321
+ color: rgba(255,255,255,.86);
322
+ font-size: 13px;
323
+ display:flex;
324
+ gap:10px;
325
+ align-items:center;
326
+ backdrop-filter: blur(10px);
327
+ -webkit-backdrop-filter: blur(10px);
328
+ box-shadow: 0 10px 40px rgba(0,0,0,.35);
329
+ opacity:0;
330
+ pointer-events:none;
331
+ transition: opacity .22s ease, transform .22s ease;
332
+ }
333
+ .toast.show{
334
+ opacity:1;
335
+ transform: translateX(-50%) translateY(-2px);
336
+ }
337
+ .toast .okdot{
338
+ width:10px;height:10px;border-radius:50%;
339
+ background: var(--good);
340
+ box-shadow: 0 0 0 6px rgba(46,229,157,.10);
341
+ }
342
+
343
+ /* Reduce motion */
344
+ @media (prefers-reduced-motion: reduce){
345
+ .blob, .dot, .wifi path, .wifi circle, .card{ animation:none !important; }
346
+ .toast{ transition:none; }
347
+ }
85
348
  </style>
86
349
  </head>
350
+
87
351
  <body>
88
- <div class="box">
89
- <h1>\u26A1 You\u2019re Offline</h1>
90
- <p>No internet connection detected. You can retry, or go back to the homepage (if cached).</p>
91
- <div class="row">
92
- <button onclick="location.reload()">Retry</button>
93
- <a href="/">Go Home</a>
94
- </div>
352
+ <div class="blob b1"></div>
353
+ <div class="blob b2"></div>
354
+ <div class="blob b3"></div>
355
+
356
+ <main class="wrap" role="main">
357
+ <section class="card" aria-labelledby="offline-title">
358
+ <div class="top">
359
+ <div>
360
+ <h1 id="offline-title">\u{1F635}\u200D\u{1F4AB} You\u2019re Offline</h1>
361
+ <p>
362
+ No internet connection detected. Don\u2019t worry \u2014 you can retry now,
363
+ or go home if it\u2019s already cached. \u2728
364
+ </p>
365
+ </div>
366
+
367
+ <div class="badge" aria-live="polite">
368
+ <span class="dot" aria-hidden="true"></span>
369
+ <span id="netText">Offline mode</span>
370
+ </div>
371
+ </div>
372
+
373
+ <div class="hero">
374
+ <div class="wifi" aria-hidden="true">
375
+ <svg width="30" height="30" viewBox="0 0 24 24" fill="none">
376
+ <path d="M2.5 9.2C8.6 3.2 15.4 3.2 21.5 9.2" stroke="white" stroke-width="2" stroke-linecap="round"/>
377
+ <path d="M5.8 12.4C10.1 8.3 13.9 8.3 18.2 12.4" stroke="white" stroke-width="2" stroke-linecap="round"/>
378
+ <path d="M9.2 15.7C11.2 13.9 12.8 13.9 14.8 15.7" stroke="white" stroke-width="2" stroke-linecap="round"/>
379
+ <circle cx="12" cy="19" r="1.4" fill="white"/>
380
+ </svg>
381
+ </div>
382
+
383
+ <div>
384
+ <p style="margin:0 0 6px;color:rgba(255,255,255,.86);font-weight:650">
385
+ Tip: Your cached pages may still open \u26A1
386
+ </p>
387
+ <p style="margin:0;color:rgba(255,255,255,.62)">
388
+ We\u2019ll auto-refresh when you\u2019re back online \u2705
389
+ </p>
390
+ </div>
391
+ </div>
392
+
393
+ <div class="actions">
394
+ <button class="btn primary" id="retryBtn" type="button">
395
+ \u{1F504} Retry
396
+ </button>
397
+ <a class="btn ghost" href="/" rel="noopener">
398
+ \u{1F3E0} Go Home
399
+ </a>
400
+ <button class="btn ghost" id="copyBtn" type="button" title="Copy current URL">
401
+ \u{1F517} Copy URL
402
+ </button>
403
+ </div>
404
+
405
+ <div class="hint">
406
+ <span>Quick keys:</span>
407
+ <span class="kbd">R</span> Retry
408
+ <span class="kbd">H</span> Home
409
+ </div>
410
+ </section>
411
+ </main>
412
+
413
+ <div class="toast" id="toast" role="status" aria-live="polite">
414
+ <span class="okdot" aria-hidden="true"></span>
415
+ <span id="toastText">Back online \u2014 reloading\u2026</span>
95
416
  </div>
417
+
418
+ <script>
419
+ const retryBtn = document.getElementById("retryBtn");
420
+ const copyBtn = document.getElementById("copyBtn");
421
+ const netText = document.getElementById("netText");
422
+ const toast = document.getElementById("toast");
423
+ const toastText= document.getElementById("toastText");
424
+
425
+ function showToast(msg){
426
+ toastText.textContent = msg;
427
+ toast.classList.add("show");
428
+ clearTimeout(showToast._t);
429
+ showToast._t = setTimeout(() => toast.classList.remove("show"), 2200);
430
+ }
431
+
432
+ function updateStatus(){
433
+ const online = navigator.onLine;
434
+ netText.textContent = online ? "Online" : "Offline mode";
435
+ // If it becomes online, reload after tiny delay (feels smoother)
436
+ if (online){
437
+ showToast("\u2705 Back online \u2014 reloading\u2026");
438
+ setTimeout(() => location.reload(), 650);
439
+ }
440
+ }
441
+
442
+ retryBtn.addEventListener("click", () => location.reload());
443
+
444
+ copyBtn.addEventListener("click", async () => {
445
+ try{
446
+ await navigator.clipboard.writeText(location.href);
447
+ showToast("\u{1F4CB} URL copied!");
448
+ }catch{
449
+ // fallback
450
+ const ta = document.createElement("textarea");
451
+ ta.value = location.href;
452
+ document.body.appendChild(ta);
453
+ ta.select();
454
+ document.execCommand("copy");
455
+ ta.remove();
456
+ showToast("\u{1F4CB} URL copied!");
457
+ }
458
+ });
459
+
460
+ window.addEventListener("online", updateStatus);
461
+ window.addEventListener("offline", updateStatus);
462
+
463
+ // Keyboard shortcuts
464
+ window.addEventListener("keydown", (e) => {
465
+ const k = (e.key || "").toLowerCase();
466
+ if (k === "r") location.reload();
467
+ if (k === "h") location.href = "/";
468
+ });
469
+
470
+ // initial
471
+ updateStatus();
472
+ </script>
96
473
  </body>
97
474
  </html>`;
98
475
  }
package/dist/cli.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts","../src/core/utils.ts","../src/core/offlineHtml.ts","../src/core/offlineSvg.ts","../src/core/swTemplate.ts"],"sourcesContent":["#!/usr/bin/env node\r\nimport path from \"node:path\";\r\nimport { parseArgs, splitList, writeFileSafe, exists } from \"./core/utils\";\r\nimport { offlineHtmlTemplate } from \"./core/offlineHtml\";\r\nimport { offlineSvgTemplate } from \"./core/offlineSvg\";\r\nimport { buildServiceWorkerJS } from \"./core/swTemplate\";\r\nimport type { OfflineKitBuildOptions } from \"./types\";\r\n\r\nfunction normalizePublicPath(p: string) {\r\n if (!p.startsWith(\"/\")) return \"/\" + p;\r\n return p;\r\n}\r\n\r\nfunction withDefaults(o: OfflineKitBuildOptions): Required<OfflineKitBuildOptions> {\r\n return {\r\n outDir: o.outDir ?? \"public\",\r\n swFileName: o.swFileName ?? \"sw.js\",\r\n offlinePage: normalizePublicPath(o.offlinePage ?? \"/offline.html\"),\r\n offlineImage: normalizePublicPath(o.offlineImage ?? \"/offline.svg\"),\r\n cacheName: o.cacheName ?? \"offline-page-kit\",\r\n precache: o.precache ?? [],\r\n htmlStrategy: o.htmlStrategy ?? \"networkFirst\",\r\n assetStrategy: o.assetStrategy ?? \"staleWhileRevalidate\",\r\n imageStrategy: o.imageStrategy ?? \"cacheFirst\",\r\n assetExtensions: o.assetExtensions ?? [],\r\n apiPrefixes: o.apiPrefixes ?? [],\r\n };\r\n}\r\n\r\nconst args = parseArgs(process.argv.slice(2));\r\nconst cmd = process.argv.slice(2).find(a => !a.startsWith(\"--\")) || \"init\";\r\n\r\nconst outDir = path.resolve(process.cwd(), args.get(\"outDir\") || \"public\");\r\n\r\nconst options = withDefaults({\r\n outDir,\r\n swFileName: args.get(\"swFileName\") || \"sw.js\",\r\n offlinePage: args.get(\"offlinePage\") || \"/offline.html\",\r\n offlineImage: args.get(\"offlineImage\") || \"/offline.svg\",\r\n cacheName: args.get(\"cacheName\") || \"offline-page-kit\",\r\n precache: splitList(args.get(\"precache\")),\r\n htmlStrategy: (args.get(\"htmlStrategy\") as any) || \"networkFirst\",\r\n assetStrategy: (args.get(\"assetStrategy\") as any) || \"staleWhileRevalidate\",\r\n imageStrategy: (args.get(\"imageStrategy\") as any) || \"cacheFirst\",\r\n assetExtensions: splitList(args.get(\"assetExtensions\")),\r\n apiPrefixes: splitList(args.get(\"apiPrefixes\")),\r\n} as OfflineKitBuildOptions);\r\n\r\nconst swOut = path.join(outDir, options.swFileName);\r\nconst offlineHtmlOut = path.join(outDir, options.offlinePage.replace(/^\\//, \"\"));\r\nconst offlineSvgOut = path.join(outDir, options.offlineImage.replace(/^\\//, \"\"));\r\n\r\nif (cmd === \"init\" || cmd === \"build\") {\r\n // generate offline page if missing OR force (when build)\r\n if (cmd === \"build\" || !exists(offlineHtmlOut)) {\r\n writeFileSafe(offlineHtmlOut, offlineHtmlTemplate());\r\n }\r\n if (cmd === \"build\" || !exists(offlineSvgOut)) {\r\n writeFileSafe(offlineSvgOut, offlineSvgTemplate());\r\n }\r\n\r\n const sw = buildServiceWorkerJS(options);\r\n writeFileSafe(swOut, sw);\r\n\r\n console.log(`[offline-page-kit] Generated:\r\n- ${swOut}\r\n- ${offlineHtmlOut}\r\n- ${offlineSvgOut}\r\n`);\r\n} else {\r\n console.log(`[offline-page-kit] Unknown command: ${cmd}\r\nUse:\r\n offline-page-kit init --outDir public\r\n offline-page-kit build --outDir public\r\n`);\r\n}","import fs from \"node:fs\";\r\nimport path from \"node:path\";\r\n\r\nexport function ensureDir(p: string) {\r\n fs.mkdirSync(p, { recursive: true });\r\n}\r\n\r\nexport function writeFileSafe(filePath: string, content: string) {\r\n ensureDir(path.dirname(filePath));\r\n fs.writeFileSync(filePath, content, \"utf8\");\r\n}\r\n\r\nexport function exists(filePath: string) {\r\n try { fs.accessSync(filePath); return true; } catch { return false; }\r\n}\r\n\r\nexport function parseArgs(argv: string[]) {\r\n const m = new Map<string, string>();\r\n\r\n const norm = (k: string) => k.replace(/-/g, \"\").toLowerCase();\r\n\r\n for (let i = 0; i < argv.length; i++) {\r\n const a = argv[i];\r\n if (!a.startsWith(\"--\")) continue;\r\n\r\n const rawKey = a.slice(2);\r\n const key = norm(rawKey);\r\n\r\n const value = argv[i + 1] && !argv[i + 1].startsWith(\"--\") ? argv[++i] : \"true\";\r\n m.set(key, value);\r\n }\r\n\r\n return {\r\n get(name: string) {\r\n return m.get(norm(name));\r\n }\r\n } as unknown as Map<string, string>;\r\n}\r\n\r\nexport function splitList(v: string | undefined) {\r\n return (v || \"\")\r\n .split(\",\")\r\n .map(s => s.trim())\r\n .filter(Boolean);\r\n}","export function offlineHtmlTemplate(title = \"You're Offline\") {\r\n return `<!doctype html>\r\n<html lang=\"en\">\r\n<head>\r\n <meta charset=\"UTF-8\"/>\r\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"/>\r\n <title>${title}</title>\r\n <style>\r\n body{margin:0;height:100vh;display:grid;place-items:center;font-family:system-ui,-apple-system,Segoe UI,Roboto;background:#0b0f19;color:#fff}\r\n .box{max-width:560px;padding:28px 26px;border-radius:18px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.04)}\r\n h1{margin:0 0 10px;font-size:28px}\r\n p{margin:0 0 18px;opacity:.85;line-height:1.5}\r\n .row{display:flex;gap:10px;flex-wrap:wrap}\r\n button,a{appearance:none;border:0;border-radius:12px;padding:10px 14px;cursor:pointer;text-decoration:none}\r\n button{background:#fff;color:#111}\r\n a{background:rgba(255,255,255,.1);color:#fff}\r\n </style>\r\n</head>\r\n<body>\r\n <div class=\"box\">\r\n <h1>⚡ You’re Offline</h1>\r\n <p>No internet connection detected. You can retry, or go back to the homepage (if cached).</p>\r\n <div class=\"row\">\r\n <button onclick=\"location.reload()\">Retry</button>\r\n <a href=\"/\">Go Home</a>\r\n </div>\r\n </div>\r\n</body>\r\n</html>`;\r\n}","export function offlineSvgTemplate() {\r\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1200\" height=\"630\" viewBox=\"0 0 1200 630\">\r\n <rect width=\"1200\" height=\"630\" fill=\"#0b0f19\"/>\r\n <text x=\"80\" y=\"220\" fill=\"#ffffff\" font-size=\"64\" font-family=\"system-ui, -apple-system, Segoe UI, Roboto\">You’re Offline</text>\r\n <text x=\"80\" y=\"300\" fill=\"#cbd5e1\" font-size=\"28\" font-family=\"system-ui, -apple-system, Segoe UI, Roboto\">Please check your connection and try again.</text>\r\n <circle cx=\"1040\" cy=\"220\" r=\"90\" fill=\"rgba(255,255,255,0.08)\"/>\r\n <path d=\"M980 220c40-40 120-40 160 0\" stroke=\"#fff\" stroke-width=\"10\" fill=\"none\" opacity=\"0.6\"/>\r\n <path d=\"M1010 250c25-25 75-25 100 0\" stroke=\"#fff\" stroke-width=\"10\" fill=\"none\" opacity=\"0.6\"/>\r\n <circle cx=\"1060\" cy=\"290\" r=\"10\" fill=\"#fff\" opacity=\"0.7\"/>\r\n</svg>`;\r\n}","import type { OfflineKitBuildOptions } from \"../types\";\r\n\r\nconst js = (v: unknown) => JSON.stringify(v);\r\n\r\nexport function buildServiceWorkerJS(options: Required<OfflineKitBuildOptions>) {\r\n const { cacheName, offlinePage, offlineImage } = options;\r\n\r\n return `/* offline-page-kit service worker (minimal) */\r\nconst CACHE_NAME = ${js(cacheName)};\r\nconst OFFLINE_PAGE = ${js(offlinePage)};\r\nconst OFFLINE_IMAGE = ${js(offlineImage)};\r\n\r\nself.addEventListener(\"install\", (event) => {\r\n event.waitUntil((async () => {\r\n const cache = await caches.open(CACHE_NAME);\r\n\r\n // ✅ Do not let one 404 kill the install\r\n await Promise.allSettled([\r\n cache.add(OFFLINE_PAGE),\r\n cache.add(OFFLINE_IMAGE),\r\n ]);\r\n\r\n await self.skipWaiting();\r\n })());\r\n});\r\n\r\nself.addEventListener(\"activate\", (event) => {\r\n event.waitUntil((async () => {\r\n await self.clients.claim();\r\n })());\r\n});\r\n\r\nself.addEventListener(\"fetch\", (event) => {\r\n const req = event.request;\r\n\r\n // ✅ Offline fallback only for page navigations\r\n if (req.mode === \"navigate\") {\r\n event.respondWith((async () => {\r\n try {\r\n return await fetch(req);\r\n } catch {\r\n const cache = await caches.open(CACHE_NAME);\r\n return (await cache.match(OFFLINE_PAGE)) || new Response(\"Offline\", { status: 503 });\r\n }\r\n })());\r\n }\r\n});\r\n`;\r\n}"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AACA,IAAAA,oBAAiB;;;ACDjB,qBAAe;AACf,uBAAiB;AAEV,SAAS,UAAU,GAAW;AACjC,iBAAAC,QAAG,UAAU,GAAG,EAAE,WAAW,KAAK,CAAC;AACvC;AAEO,SAAS,cAAc,UAAkB,SAAiB;AAC7D,YAAU,iBAAAC,QAAK,QAAQ,QAAQ,CAAC;AAChC,iBAAAD,QAAG,cAAc,UAAU,SAAS,MAAM;AAC9C;AAEO,SAAS,OAAO,UAAkB;AACrC,MAAI;AAAE,mBAAAA,QAAG,WAAW,QAAQ;AAAG,WAAO;AAAA,EAAM,QAAQ;AAAE,WAAO;AAAA,EAAO;AACxE;AAEO,SAAS,UAAU,MAAgB;AACtC,QAAM,IAAI,oBAAI,IAAoB;AAElC,QAAM,OAAO,CAAC,MAAc,EAAE,QAAQ,MAAM,EAAE,EAAE,YAAY;AAE5D,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AAClC,UAAM,IAAI,KAAK,CAAC;AAChB,QAAI,CAAC,EAAE,WAAW,IAAI,EAAG;AAEzB,UAAM,SAAS,EAAE,MAAM,CAAC;AACxB,UAAM,MAAM,KAAK,MAAM;AAEvB,UAAM,QAAQ,KAAK,IAAI,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,EAAE,WAAW,IAAI,IAAI,KAAK,EAAE,CAAC,IAAI;AACzE,MAAE,IAAI,KAAK,KAAK;AAAA,EACpB;AAEA,SAAO;AAAA,IACH,IAAI,MAAc;AACd,aAAO,EAAE,IAAI,KAAK,IAAI,CAAC;AAAA,IAC3B;AAAA,EACJ;AACJ;AAEO,SAAS,UAAU,GAAuB;AAC7C,UAAQ,KAAK,IACR,MAAM,GAAG,EACT,IAAI,OAAK,EAAE,KAAK,CAAC,EACjB,OAAO,OAAO;AACvB;;;AC5CO,SAAS,oBAAoB,QAAQ,kBAAkB;AAC1D,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA,WAKA,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAuBhB;;;AC7BO,SAAS,qBAAqB;AACnC,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAST;;;ACRA,IAAM,KAAK,CAAC,MAAe,KAAK,UAAU,CAAC;AAEpC,SAAS,qBAAqBE,UAA2C;AAC9E,QAAM,EAAE,WAAW,aAAa,aAAa,IAAIA;AAEjD,SAAO;AAAA,qBACY,GAAG,SAAS,CAAC;AAAA,uBACX,GAAG,WAAW,CAAC;AAAA,wBACd,GAAG,YAAY,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAsCxC;;;AJxCA,SAAS,oBAAoB,GAAW;AACpC,MAAI,CAAC,EAAE,WAAW,GAAG,EAAG,QAAO,MAAM;AACrC,SAAO;AACX;AAEA,SAAS,aAAa,GAA6D;AAC/E,SAAO;AAAA,IACH,QAAQ,EAAE,UAAU;AAAA,IACpB,YAAY,EAAE,cAAc;AAAA,IAC5B,aAAa,oBAAoB,EAAE,eAAe,eAAe;AAAA,IACjE,cAAc,oBAAoB,EAAE,gBAAgB,cAAc;AAAA,IAClE,WAAW,EAAE,aAAa;AAAA,IAC1B,UAAU,EAAE,YAAY,CAAC;AAAA,IACzB,cAAc,EAAE,gBAAgB;AAAA,IAChC,eAAe,EAAE,iBAAiB;AAAA,IAClC,eAAe,EAAE,iBAAiB;AAAA,IAClC,iBAAiB,EAAE,mBAAmB,CAAC;AAAA,IACvC,aAAa,EAAE,eAAe,CAAC;AAAA,EACnC;AACJ;AAEA,IAAM,OAAO,UAAU,QAAQ,KAAK,MAAM,CAAC,CAAC;AAC5C,IAAM,MAAM,QAAQ,KAAK,MAAM,CAAC,EAAE,KAAK,OAAK,CAAC,EAAE,WAAW,IAAI,CAAC,KAAK;AAEpE,IAAM,SAAS,kBAAAC,QAAK,QAAQ,QAAQ,IAAI,GAAG,KAAK,IAAI,QAAQ,KAAK,QAAQ;AAEzE,IAAM,UAAU,aAAa;AAAA,EACzB;AAAA,EACA,YAAY,KAAK,IAAI,YAAY,KAAK;AAAA,EACtC,aAAa,KAAK,IAAI,aAAa,KAAK;AAAA,EACxC,cAAc,KAAK,IAAI,cAAc,KAAK;AAAA,EAC1C,WAAW,KAAK,IAAI,WAAW,KAAK;AAAA,EACpC,UAAU,UAAU,KAAK,IAAI,UAAU,CAAC;AAAA,EACxC,cAAe,KAAK,IAAI,cAAc,KAAa;AAAA,EACnD,eAAgB,KAAK,IAAI,eAAe,KAAa;AAAA,EACrD,eAAgB,KAAK,IAAI,eAAe,KAAa;AAAA,EACrD,iBAAiB,UAAU,KAAK,IAAI,iBAAiB,CAAC;AAAA,EACtD,aAAa,UAAU,KAAK,IAAI,aAAa,CAAC;AAClD,CAA2B;AAE3B,IAAM,QAAQ,kBAAAA,QAAK,KAAK,QAAQ,QAAQ,UAAU;AAClD,IAAM,iBAAiB,kBAAAA,QAAK,KAAK,QAAQ,QAAQ,YAAY,QAAQ,OAAO,EAAE,CAAC;AAC/E,IAAM,gBAAgB,kBAAAA,QAAK,KAAK,QAAQ,QAAQ,aAAa,QAAQ,OAAO,EAAE,CAAC;AAE/E,IAAI,QAAQ,UAAU,QAAQ,SAAS;AAEnC,MAAI,QAAQ,WAAW,CAAC,OAAO,cAAc,GAAG;AAC5C,kBAAc,gBAAgB,oBAAoB,CAAC;AAAA,EACvD;AACA,MAAI,QAAQ,WAAW,CAAC,OAAO,aAAa,GAAG;AAC3C,kBAAc,eAAe,mBAAmB,CAAC;AAAA,EACrD;AAEA,QAAM,KAAK,qBAAqB,OAAO;AACvC,gBAAc,OAAO,EAAE;AAEvB,UAAQ,IAAI;AAAA,IACZ,KAAK;AAAA,IACL,cAAc;AAAA,IACd,aAAa;AAAA,CAChB;AACD,OAAO;AACH,UAAQ,IAAI,uCAAuC,GAAG;AAAA;AAAA;AAAA;AAAA,CAIzD;AACD;","names":["import_node_path","fs","path","options","path"]}
1
+ {"version":3,"sources":["../src/cli.ts","../src/core/utils.ts","../src/core/offlineHtml.ts","../src/core/offlineSvg.ts","../src/core/swTemplate.ts"],"sourcesContent":["#!/usr/bin/env node\r\nimport path from \"node:path\";\r\nimport { parseArgs, splitList, writeFileSafe, exists } from \"./core/utils\";\r\nimport { offlineHtmlTemplate } from \"./core/offlineHtml\";\r\nimport { offlineSvgTemplate } from \"./core/offlineSvg\";\r\nimport { buildServiceWorkerJS } from \"./core/swTemplate\";\r\nimport type { OfflineKitBuildOptions } from \"./types\";\r\n\r\nfunction normalizePublicPath(p: string) {\r\n if (!p.startsWith(\"/\")) return \"/\" + p;\r\n return p;\r\n}\r\n\r\nfunction withDefaults(o: OfflineKitBuildOptions): Required<OfflineKitBuildOptions> {\r\n return {\r\n outDir: o.outDir ?? \"public\",\r\n swFileName: o.swFileName ?? \"sw.js\",\r\n offlinePage: normalizePublicPath(o.offlinePage ?? \"/offline.html\"),\r\n offlineImage: normalizePublicPath(o.offlineImage ?? \"/offline.svg\"),\r\n cacheName: o.cacheName ?? \"offline-page-kit\",\r\n precache: o.precache ?? [],\r\n htmlStrategy: o.htmlStrategy ?? \"networkFirst\",\r\n assetStrategy: o.assetStrategy ?? \"staleWhileRevalidate\",\r\n imageStrategy: o.imageStrategy ?? \"cacheFirst\",\r\n assetExtensions: o.assetExtensions ?? [],\r\n apiPrefixes: o.apiPrefixes ?? [],\r\n };\r\n}\r\n\r\nconst args = parseArgs(process.argv.slice(2));\r\nconst cmd = process.argv.slice(2).find(a => !a.startsWith(\"--\")) || \"init\";\r\n\r\nconst outDir = path.resolve(process.cwd(), args.get(\"outDir\") || \"public\");\r\n\r\nconst options = withDefaults({\r\n outDir,\r\n swFileName: args.get(\"swFileName\") || \"sw.js\",\r\n offlinePage: args.get(\"offlinePage\") || \"/offline.html\",\r\n offlineImage: args.get(\"offlineImage\") || \"/offline.svg\",\r\n cacheName: args.get(\"cacheName\") || \"offline-page-kit\",\r\n precache: splitList(args.get(\"precache\")),\r\n htmlStrategy: (args.get(\"htmlStrategy\") as any) || \"networkFirst\",\r\n assetStrategy: (args.get(\"assetStrategy\") as any) || \"staleWhileRevalidate\",\r\n imageStrategy: (args.get(\"imageStrategy\") as any) || \"cacheFirst\",\r\n assetExtensions: splitList(args.get(\"assetExtensions\")),\r\n apiPrefixes: splitList(args.get(\"apiPrefixes\")),\r\n} as OfflineKitBuildOptions);\r\n\r\nconst swOut = path.join(outDir, options.swFileName);\r\nconst offlineHtmlOut = path.join(outDir, options.offlinePage.replace(/^\\//, \"\"));\r\nconst offlineSvgOut = path.join(outDir, options.offlineImage.replace(/^\\//, \"\"));\r\n\r\nif (cmd === \"init\" || cmd === \"build\") {\r\n // generate offline page if missing OR force (when build)\r\n if (cmd === \"build\" || !exists(offlineHtmlOut)) {\r\n writeFileSafe(offlineHtmlOut, offlineHtmlTemplate());\r\n }\r\n if (cmd === \"build\" || !exists(offlineSvgOut)) {\r\n writeFileSafe(offlineSvgOut, offlineSvgTemplate());\r\n }\r\n\r\n const sw = buildServiceWorkerJS(options);\r\n writeFileSafe(swOut, sw);\r\n\r\n console.log(`[offline-page-kit] Generated:\r\n- ${swOut}\r\n- ${offlineHtmlOut}\r\n- ${offlineSvgOut}\r\n`);\r\n} else {\r\n console.log(`[offline-page-kit] Unknown command: ${cmd}\r\nUse:\r\n offline-page-kit init --outDir public\r\n offline-page-kit build --outDir public\r\n`);\r\n}","import fs from \"node:fs\";\r\nimport path from \"node:path\";\r\n\r\nexport function ensureDir(p: string) {\r\n fs.mkdirSync(p, { recursive: true });\r\n}\r\n\r\nexport function writeFileSafe(filePath: string, content: string) {\r\n ensureDir(path.dirname(filePath));\r\n fs.writeFileSync(filePath, content, \"utf8\");\r\n}\r\n\r\nexport function exists(filePath: string) {\r\n try { fs.accessSync(filePath); return true; } catch { return false; }\r\n}\r\n\r\nexport function parseArgs(argv: string[]) {\r\n const m = new Map<string, string>();\r\n\r\n const norm = (k: string) => k.replace(/-/g, \"\").toLowerCase();\r\n\r\n for (let i = 0; i < argv.length; i++) {\r\n const a = argv[i];\r\n if (!a.startsWith(\"--\")) continue;\r\n\r\n const rawKey = a.slice(2);\r\n const key = norm(rawKey);\r\n\r\n const value = argv[i + 1] && !argv[i + 1].startsWith(\"--\") ? argv[++i] : \"true\";\r\n m.set(key, value);\r\n }\r\n\r\n return {\r\n get(name: string) {\r\n return m.get(norm(name));\r\n }\r\n } as unknown as Map<string, string>;\r\n}\r\n\r\nexport function splitList(v: string | undefined) {\r\n return (v || \"\")\r\n .split(\",\")\r\n .map(s => s.trim())\r\n .filter(Boolean);\r\n}","export function offlineHtmlTemplate(title = \"You're Offline\") {\r\n return `<!doctype html>\r\n<html lang=\"en\">\r\n<head>\r\n <meta charset=\"UTF-8\"/>\r\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"/>\r\n <title>${title}</title>\r\n <meta name=\"theme-color\" content=\"#0b0f19\"/>\r\n\r\n <style>\r\n :root{\r\n --bg0:#060912;\r\n --bg1:#0b1022;\r\n --card: rgba(255,255,255,.06);\r\n --card2: rgba(255,255,255,.04);\r\n --stroke: rgba(255,255,255,.14);\r\n --text:#ffffff;\r\n --muted: rgba(255,255,255,.78);\r\n --muted2: rgba(255,255,255,.62);\r\n --good:#2ee59d;\r\n --warn:#ffd166;\r\n --shadow: 0 20px 80px rgba(0,0,0,.55);\r\n --radius: 22px;\r\n }\r\n\r\n *{box-sizing:border-box}\r\n html,body{height:100%}\r\n body{\r\n margin:0;\r\n font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, \"Apple Color Emoji\",\"Segoe UI Emoji\";\r\n color:var(--text);\r\n background:\r\n radial-gradient(900px 500px at 18% 18%, rgba(120,140,255,.20), transparent 60%),\r\n radial-gradient(700px 500px at 82% 20%, rgba(46,229,157,.16), transparent 58%),\r\n radial-gradient(600px 520px at 50% 95%, rgba(255,209,102,.14), transparent 55%),\r\n linear-gradient(180deg, var(--bg0), var(--bg1));\r\n overflow:hidden;\r\n }\r\n\r\n /* Floating blobs (subtle animation) */\r\n .blob{\r\n position:absolute; inset:auto;\r\n width: 520px; height: 520px;\r\n border-radius: 50%;\r\n filter: blur(42px);\r\n opacity:.35;\r\n animation: float 10s ease-in-out infinite;\r\n pointer-events:none;\r\n transform: translate3d(0,0,0);\r\n }\r\n .blob.b1{left:-180px; top:-200px; background: rgba(120,140,255,.55); animation-duration: 12s;}\r\n .blob.b2{right:-210px; top:-150px; background: rgba(46,229,157,.52); animation-duration: 14s;}\r\n .blob.b3{left:18%; bottom:-320px; background: rgba(255,209,102,.45); animation-duration: 16s;}\r\n\r\n @keyframes float{\r\n 0%,100%{ transform: translate(0,0) scale(1);}\r\n 50%{ transform: translate(22px,-18px) scale(1.06);}\r\n }\r\n\r\n /* Main layout */\r\n .wrap{\r\n min-height:100%;\r\n display:grid;\r\n place-items:center;\r\n padding: 24px;\r\n position:relative;\r\n z-index:1;\r\n }\r\n\r\n .card{\r\n width:min(720px, 100%);\r\n border-radius: var(--radius);\r\n background: linear-gradient(180deg, var(--card), var(--card2));\r\n border:1px solid var(--stroke);\r\n box-shadow: var(--shadow);\r\n padding: 26px;\r\n backdrop-filter: blur(10px);\r\n -webkit-backdrop-filter: blur(10px);\r\n\r\n animation: pop .55s cubic-bezier(.2,.9,.2,1) both;\r\n }\r\n\r\n @keyframes pop{\r\n from{ opacity:0; transform: translateY(14px) scale(.98); }\r\n to{ opacity:1; transform: translateY(0) scale(1); }\r\n }\r\n\r\n .top{\r\n display:flex;\r\n align-items:flex-start;\r\n justify-content:space-between;\r\n gap:16px;\r\n margin-bottom: 14px;\r\n }\r\n\r\n .badge{\r\n display:inline-flex;\r\n align-items:center;\r\n gap:10px;\r\n padding: 10px 12px;\r\n border-radius: 999px;\r\n border:1px solid rgba(255,255,255,.14);\r\n background: rgba(0,0,0,.18);\r\n color: var(--muted);\r\n font-size: 13px;\r\n user-select:none;\r\n white-space:nowrap;\r\n }\r\n\r\n /* Ping dot */\r\n .dot{\r\n width:10px;height:10px;border-radius:50%;\r\n background: var(--warn);\r\n box-shadow: 0 0 0 0 rgba(255,209,102,.65);\r\n animation: ping 1.6s infinite;\r\n }\r\n @keyframes ping{\r\n 0%{ box-shadow: 0 0 0 0 rgba(255,209,102,.55); }\r\n 70%{ box-shadow: 0 0 0 10px rgba(255,209,102,0); }\r\n 100%{ box-shadow: 0 0 0 0 rgba(255,209,102,0); }\r\n }\r\n\r\n h1{\r\n margin: 0 0 8px;\r\n font-size: clamp(26px, 3.3vw, 34px);\r\n letter-spacing: -0.02em;\r\n line-height: 1.15;\r\n }\r\n\r\n p{\r\n margin: 0;\r\n color: var(--muted2);\r\n line-height: 1.55;\r\n font-size: 15.5px;\r\n }\r\n\r\n .hero{\r\n display:flex;\r\n align-items:center;\r\n gap: 14px;\r\n margin-top: 10px;\r\n }\r\n\r\n /* Animated wifi icon */\r\n .wifi{\r\n width: 54px; height: 54px;\r\n border-radius: 16px;\r\n display:grid; place-items:center;\r\n background: rgba(255,255,255,.06);\r\n border:1px solid rgba(255,255,255,.12);\r\n position:relative;\r\n overflow:hidden;\r\n }\r\n .wifi::after{\r\n content:\"\";\r\n position:absolute; inset:-40%;\r\n background: radial-gradient(circle at 30% 30%, rgba(255,255,255,.18), transparent 55%);\r\n animation: sheen 2.8s ease-in-out infinite;\r\n }\r\n @keyframes sheen{\r\n 0%,100%{ transform: translate(-8px,-6px) rotate(0deg); opacity:.75;}\r\n 50%{ transform: translate(10px,8px) rotate(8deg); opacity:.9;}\r\n }\r\n\r\n .wifi svg{ position:relative; z-index:1; opacity:.95; }\r\n .wifi path{ animation: wave 1.6s ease-in-out infinite; transform-origin:center; }\r\n .wifi path:nth-child(1){ opacity:.35; animation-delay:.05s;}\r\n .wifi path:nth-child(2){ opacity:.55; animation-delay:.12s;}\r\n .wifi path:nth-child(3){ opacity:.75; animation-delay:.2s;}\r\n .wifi circle{ opacity:.9; animation: blink 1.2s ease-in-out infinite; }\r\n\r\n @keyframes wave{\r\n 0%,100%{ transform: scale(1); }\r\n 50%{ transform: scale(1.06); }\r\n }\r\n @keyframes blink{\r\n 0%,100%{ opacity:.9; }\r\n 50%{ opacity:.45; }\r\n }\r\n\r\n .actions{\r\n display:flex;\r\n flex-wrap:wrap;\r\n gap: 10px;\r\n margin-top: 18px;\r\n }\r\n\r\n .btn{\r\n appearance:none;\r\n border:0;\r\n border-radius: 14px;\r\n padding: 11px 14px;\r\n cursor:pointer;\r\n text-decoration:none;\r\n display:inline-flex;\r\n align-items:center;\r\n gap:10px;\r\n font-weight: 650;\r\n font-size: 14.5px;\r\n letter-spacing: .01em;\r\n transition: transform .12s ease, filter .12s ease, background .2s ease, border-color .2s ease;\r\n user-select:none;\r\n }\r\n .btn:active{ transform: translateY(1px) scale(.99); }\r\n\r\n .primary{\r\n background: #ffffff;\r\n color: #0b0f19;\r\n box-shadow: 0 8px 24px rgba(255,255,255,.12);\r\n }\r\n .primary:hover{ filter: brightness(0.98); }\r\n\r\n .ghost{\r\n background: rgba(255,255,255,.08);\r\n color: var(--text);\r\n border: 1px solid rgba(255,255,255,.14);\r\n }\r\n .ghost:hover{\r\n background: rgba(255,255,255,.10);\r\n border-color: rgba(255,255,255,.18);\r\n }\r\n\r\n .hint{\r\n margin-top: 14px;\r\n display:flex;\r\n gap:10px;\r\n flex-wrap:wrap;\r\n align-items:center;\r\n color: var(--muted2);\r\n font-size: 13px;\r\n }\r\n\r\n .kbd{\r\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", monospace;\r\n font-size: 12px;\r\n padding: 4px 8px;\r\n border-radius: 9px;\r\n border:1px solid rgba(255,255,255,.14);\r\n background: rgba(0,0,0,.20);\r\n color: rgba(255,255,255,.82);\r\n }\r\n\r\n /* Toast */\r\n .toast{\r\n position: fixed;\r\n left: 50%;\r\n bottom: 18px;\r\n transform: translateX(-50%);\r\n padding: 10px 12px;\r\n border-radius: 999px;\r\n background: rgba(0,0,0,.40);\r\n border:1px solid rgba(255,255,255,.14);\r\n color: rgba(255,255,255,.86);\r\n font-size: 13px;\r\n display:flex;\r\n gap:10px;\r\n align-items:center;\r\n backdrop-filter: blur(10px);\r\n -webkit-backdrop-filter: blur(10px);\r\n box-shadow: 0 10px 40px rgba(0,0,0,.35);\r\n opacity:0;\r\n pointer-events:none;\r\n transition: opacity .22s ease, transform .22s ease;\r\n }\r\n .toast.show{\r\n opacity:1;\r\n transform: translateX(-50%) translateY(-2px);\r\n }\r\n .toast .okdot{\r\n width:10px;height:10px;border-radius:50%;\r\n background: var(--good);\r\n box-shadow: 0 0 0 6px rgba(46,229,157,.10);\r\n }\r\n\r\n /* Reduce motion */\r\n @media (prefers-reduced-motion: reduce){\r\n .blob, .dot, .wifi path, .wifi circle, .card{ animation:none !important; }\r\n .toast{ transition:none; }\r\n }\r\n </style>\r\n</head>\r\n\r\n<body>\r\n <div class=\"blob b1\"></div>\r\n <div class=\"blob b2\"></div>\r\n <div class=\"blob b3\"></div>\r\n\r\n <main class=\"wrap\" role=\"main\">\r\n <section class=\"card\" aria-labelledby=\"offline-title\">\r\n <div class=\"top\">\r\n <div>\r\n <h1 id=\"offline-title\">😵‍💫 You’re Offline</h1>\r\n <p>\r\n No internet connection detected. Don’t worry — you can retry now,\r\n or go home if it’s already cached. ✨\r\n </p>\r\n </div>\r\n\r\n <div class=\"badge\" aria-live=\"polite\">\r\n <span class=\"dot\" aria-hidden=\"true\"></span>\r\n <span id=\"netText\">Offline mode</span>\r\n </div>\r\n </div>\r\n\r\n <div class=\"hero\">\r\n <div class=\"wifi\" aria-hidden=\"true\">\r\n <svg width=\"30\" height=\"30\" viewBox=\"0 0 24 24\" fill=\"none\">\r\n <path d=\"M2.5 9.2C8.6 3.2 15.4 3.2 21.5 9.2\" stroke=\"white\" stroke-width=\"2\" stroke-linecap=\"round\"/>\r\n <path d=\"M5.8 12.4C10.1 8.3 13.9 8.3 18.2 12.4\" stroke=\"white\" stroke-width=\"2\" stroke-linecap=\"round\"/>\r\n <path d=\"M9.2 15.7C11.2 13.9 12.8 13.9 14.8 15.7\" stroke=\"white\" stroke-width=\"2\" stroke-linecap=\"round\"/>\r\n <circle cx=\"12\" cy=\"19\" r=\"1.4\" fill=\"white\"/>\r\n </svg>\r\n </div>\r\n\r\n <div>\r\n <p style=\"margin:0 0 6px;color:rgba(255,255,255,.86);font-weight:650\">\r\n Tip: Your cached pages may still open ⚡\r\n </p>\r\n <p style=\"margin:0;color:rgba(255,255,255,.62)\">\r\n We’ll auto-refresh when you’re back online ✅\r\n </p>\r\n </div>\r\n </div>\r\n\r\n <div class=\"actions\">\r\n <button class=\"btn primary\" id=\"retryBtn\" type=\"button\">\r\n 🔄 Retry\r\n </button>\r\n <a class=\"btn ghost\" href=\"/\" rel=\"noopener\">\r\n 🏠 Go Home\r\n </a>\r\n <button class=\"btn ghost\" id=\"copyBtn\" type=\"button\" title=\"Copy current URL\">\r\n 🔗 Copy URL\r\n </button>\r\n </div>\r\n\r\n <div class=\"hint\">\r\n <span>Quick keys:</span>\r\n <span class=\"kbd\">R</span> Retry\r\n <span class=\"kbd\">H</span> Home\r\n </div>\r\n </section>\r\n </main>\r\n\r\n <div class=\"toast\" id=\"toast\" role=\"status\" aria-live=\"polite\">\r\n <span class=\"okdot\" aria-hidden=\"true\"></span>\r\n <span id=\"toastText\">Back online — reloading…</span>\r\n </div>\r\n\r\n <script>\r\n const retryBtn = document.getElementById(\"retryBtn\");\r\n const copyBtn = document.getElementById(\"copyBtn\");\r\n const netText = document.getElementById(\"netText\");\r\n const toast = document.getElementById(\"toast\");\r\n const toastText= document.getElementById(\"toastText\");\r\n\r\n function showToast(msg){\r\n toastText.textContent = msg;\r\n toast.classList.add(\"show\");\r\n clearTimeout(showToast._t);\r\n showToast._t = setTimeout(() => toast.classList.remove(\"show\"), 2200);\r\n }\r\n\r\n function updateStatus(){\r\n const online = navigator.onLine;\r\n netText.textContent = online ? \"Online\" : \"Offline mode\";\r\n // If it becomes online, reload after tiny delay (feels smoother)\r\n if (online){\r\n showToast(\"✅ Back online — reloading…\");\r\n setTimeout(() => location.reload(), 650);\r\n }\r\n }\r\n\r\n retryBtn.addEventListener(\"click\", () => location.reload());\r\n\r\n copyBtn.addEventListener(\"click\", async () => {\r\n try{\r\n await navigator.clipboard.writeText(location.href);\r\n showToast(\"📋 URL copied!\");\r\n }catch{\r\n // fallback\r\n const ta = document.createElement(\"textarea\");\r\n ta.value = location.href;\r\n document.body.appendChild(ta);\r\n ta.select();\r\n document.execCommand(\"copy\");\r\n ta.remove();\r\n showToast(\"📋 URL copied!\");\r\n }\r\n });\r\n\r\n window.addEventListener(\"online\", updateStatus);\r\n window.addEventListener(\"offline\", updateStatus);\r\n\r\n // Keyboard shortcuts\r\n window.addEventListener(\"keydown\", (e) => {\r\n const k = (e.key || \"\").toLowerCase();\r\n if (k === \"r\") location.reload();\r\n if (k === \"h\") location.href = \"/\";\r\n });\r\n\r\n // initial\r\n updateStatus();\r\n </script>\r\n</body>\r\n</html>`;\r\n}","export function offlineSvgTemplate() {\r\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1200\" height=\"630\" viewBox=\"0 0 1200 630\">\r\n <rect width=\"1200\" height=\"630\" fill=\"#0b0f19\"/>\r\n <text x=\"80\" y=\"220\" fill=\"#ffffff\" font-size=\"64\" font-family=\"system-ui, -apple-system, Segoe UI, Roboto\">You’re Offline</text>\r\n <text x=\"80\" y=\"300\" fill=\"#cbd5e1\" font-size=\"28\" font-family=\"system-ui, -apple-system, Segoe UI, Roboto\">Please check your connection and try again.</text>\r\n <circle cx=\"1040\" cy=\"220\" r=\"90\" fill=\"rgba(255,255,255,0.08)\"/>\r\n <path d=\"M980 220c40-40 120-40 160 0\" stroke=\"#fff\" stroke-width=\"10\" fill=\"none\" opacity=\"0.6\"/>\r\n <path d=\"M1010 250c25-25 75-25 100 0\" stroke=\"#fff\" stroke-width=\"10\" fill=\"none\" opacity=\"0.6\"/>\r\n <circle cx=\"1060\" cy=\"290\" r=\"10\" fill=\"#fff\" opacity=\"0.7\"/>\r\n</svg>`;\r\n}","import type { OfflineKitBuildOptions } from \"../types\";\r\n\r\nconst js = (v: unknown) => JSON.stringify(v);\r\n\r\nexport function buildServiceWorkerJS(options: Required<OfflineKitBuildOptions>) {\r\n const { cacheName, offlinePage, offlineImage } = options;\r\n\r\n return `/* offline-page-kit service worker (minimal) */\r\nconst CACHE_NAME = ${js(cacheName)};\r\nconst OFFLINE_PAGE = ${js(offlinePage)};\r\nconst OFFLINE_IMAGE = ${js(offlineImage)};\r\n\r\nself.addEventListener(\"install\", (event) => {\r\n event.waitUntil((async () => {\r\n const cache = await caches.open(CACHE_NAME);\r\n\r\n // ✅ Do not let one 404 kill the install\r\n await Promise.allSettled([\r\n cache.add(OFFLINE_PAGE),\r\n cache.add(OFFLINE_IMAGE),\r\n ]);\r\n\r\n await self.skipWaiting();\r\n })());\r\n});\r\n\r\nself.addEventListener(\"activate\", (event) => {\r\n event.waitUntil((async () => {\r\n await self.clients.claim();\r\n })());\r\n});\r\n\r\nself.addEventListener(\"fetch\", (event) => {\r\n const req = event.request;\r\n\r\n // ✅ Offline fallback only for page navigations\r\n if (req.mode === \"navigate\") {\r\n event.respondWith((async () => {\r\n try {\r\n return await fetch(req);\r\n } catch {\r\n const cache = await caches.open(CACHE_NAME);\r\n return (await cache.match(OFFLINE_PAGE)) || new Response(\"Offline\", { status: 503 });\r\n }\r\n })());\r\n }\r\n});\r\n`;\r\n}"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AACA,IAAAA,oBAAiB;;;ACDjB,qBAAe;AACf,uBAAiB;AAEV,SAAS,UAAU,GAAW;AACjC,iBAAAC,QAAG,UAAU,GAAG,EAAE,WAAW,KAAK,CAAC;AACvC;AAEO,SAAS,cAAc,UAAkB,SAAiB;AAC7D,YAAU,iBAAAC,QAAK,QAAQ,QAAQ,CAAC;AAChC,iBAAAD,QAAG,cAAc,UAAU,SAAS,MAAM;AAC9C;AAEO,SAAS,OAAO,UAAkB;AACrC,MAAI;AAAE,mBAAAA,QAAG,WAAW,QAAQ;AAAG,WAAO;AAAA,EAAM,QAAQ;AAAE,WAAO;AAAA,EAAO;AACxE;AAEO,SAAS,UAAU,MAAgB;AACtC,QAAM,IAAI,oBAAI,IAAoB;AAElC,QAAM,OAAO,CAAC,MAAc,EAAE,QAAQ,MAAM,EAAE,EAAE,YAAY;AAE5D,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AAClC,UAAM,IAAI,KAAK,CAAC;AAChB,QAAI,CAAC,EAAE,WAAW,IAAI,EAAG;AAEzB,UAAM,SAAS,EAAE,MAAM,CAAC;AACxB,UAAM,MAAM,KAAK,MAAM;AAEvB,UAAM,QAAQ,KAAK,IAAI,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,EAAE,WAAW,IAAI,IAAI,KAAK,EAAE,CAAC,IAAI;AACzE,MAAE,IAAI,KAAK,KAAK;AAAA,EACpB;AAEA,SAAO;AAAA,IACH,IAAI,MAAc;AACd,aAAO,EAAE,IAAI,KAAK,IAAI,CAAC;AAAA,IAC3B;AAAA,EACJ;AACJ;AAEO,SAAS,UAAU,GAAuB;AAC7C,UAAQ,KAAK,IACR,MAAM,GAAG,EACT,IAAI,OAAK,EAAE,KAAK,CAAC,EACjB,OAAO,OAAO;AACvB;;;AC5CO,SAAS,oBAAoB,QAAQ,kBAAkB;AAC5D,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA,WAKE,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;A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","names":["import_node_path","fs","path","options","path"]}
package/dist/cli.js CHANGED
@@ -50,26 +50,403 @@ function offlineHtmlTemplate(title = "You're Offline") {
50
50
  <meta charset="UTF-8"/>
51
51
  <meta name="viewport" content="width=device-width,initial-scale=1"/>
52
52
  <title>${title}</title>
53
+ <meta name="theme-color" content="#0b0f19"/>
54
+
53
55
  <style>
54
- body{margin:0;height:100vh;display:grid;place-items:center;font-family:system-ui,-apple-system,Segoe UI,Roboto;background:#0b0f19;color:#fff}
55
- .box{max-width:560px;padding:28px 26px;border-radius:18px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.04)}
56
- h1{margin:0 0 10px;font-size:28px}
57
- p{margin:0 0 18px;opacity:.85;line-height:1.5}
58
- .row{display:flex;gap:10px;flex-wrap:wrap}
59
- button,a{appearance:none;border:0;border-radius:12px;padding:10px 14px;cursor:pointer;text-decoration:none}
60
- button{background:#fff;color:#111}
61
- a{background:rgba(255,255,255,.1);color:#fff}
56
+ :root{
57
+ --bg0:#060912;
58
+ --bg1:#0b1022;
59
+ --card: rgba(255,255,255,.06);
60
+ --card2: rgba(255,255,255,.04);
61
+ --stroke: rgba(255,255,255,.14);
62
+ --text:#ffffff;
63
+ --muted: rgba(255,255,255,.78);
64
+ --muted2: rgba(255,255,255,.62);
65
+ --good:#2ee59d;
66
+ --warn:#ffd166;
67
+ --shadow: 0 20px 80px rgba(0,0,0,.55);
68
+ --radius: 22px;
69
+ }
70
+
71
+ *{box-sizing:border-box}
72
+ html,body{height:100%}
73
+ body{
74
+ margin:0;
75
+ font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji";
76
+ color:var(--text);
77
+ background:
78
+ radial-gradient(900px 500px at 18% 18%, rgba(120,140,255,.20), transparent 60%),
79
+ radial-gradient(700px 500px at 82% 20%, rgba(46,229,157,.16), transparent 58%),
80
+ radial-gradient(600px 520px at 50% 95%, rgba(255,209,102,.14), transparent 55%),
81
+ linear-gradient(180deg, var(--bg0), var(--bg1));
82
+ overflow:hidden;
83
+ }
84
+
85
+ /* Floating blobs (subtle animation) */
86
+ .blob{
87
+ position:absolute; inset:auto;
88
+ width: 520px; height: 520px;
89
+ border-radius: 50%;
90
+ filter: blur(42px);
91
+ opacity:.35;
92
+ animation: float 10s ease-in-out infinite;
93
+ pointer-events:none;
94
+ transform: translate3d(0,0,0);
95
+ }
96
+ .blob.b1{left:-180px; top:-200px; background: rgba(120,140,255,.55); animation-duration: 12s;}
97
+ .blob.b2{right:-210px; top:-150px; background: rgba(46,229,157,.52); animation-duration: 14s;}
98
+ .blob.b3{left:18%; bottom:-320px; background: rgba(255,209,102,.45); animation-duration: 16s;}
99
+
100
+ @keyframes float{
101
+ 0%,100%{ transform: translate(0,0) scale(1);}
102
+ 50%{ transform: translate(22px,-18px) scale(1.06);}
103
+ }
104
+
105
+ /* Main layout */
106
+ .wrap{
107
+ min-height:100%;
108
+ display:grid;
109
+ place-items:center;
110
+ padding: 24px;
111
+ position:relative;
112
+ z-index:1;
113
+ }
114
+
115
+ .card{
116
+ width:min(720px, 100%);
117
+ border-radius: var(--radius);
118
+ background: linear-gradient(180deg, var(--card), var(--card2));
119
+ border:1px solid var(--stroke);
120
+ box-shadow: var(--shadow);
121
+ padding: 26px;
122
+ backdrop-filter: blur(10px);
123
+ -webkit-backdrop-filter: blur(10px);
124
+
125
+ animation: pop .55s cubic-bezier(.2,.9,.2,1) both;
126
+ }
127
+
128
+ @keyframes pop{
129
+ from{ opacity:0; transform: translateY(14px) scale(.98); }
130
+ to{ opacity:1; transform: translateY(0) scale(1); }
131
+ }
132
+
133
+ .top{
134
+ display:flex;
135
+ align-items:flex-start;
136
+ justify-content:space-between;
137
+ gap:16px;
138
+ margin-bottom: 14px;
139
+ }
140
+
141
+ .badge{
142
+ display:inline-flex;
143
+ align-items:center;
144
+ gap:10px;
145
+ padding: 10px 12px;
146
+ border-radius: 999px;
147
+ border:1px solid rgba(255,255,255,.14);
148
+ background: rgba(0,0,0,.18);
149
+ color: var(--muted);
150
+ font-size: 13px;
151
+ user-select:none;
152
+ white-space:nowrap;
153
+ }
154
+
155
+ /* Ping dot */
156
+ .dot{
157
+ width:10px;height:10px;border-radius:50%;
158
+ background: var(--warn);
159
+ box-shadow: 0 0 0 0 rgba(255,209,102,.65);
160
+ animation: ping 1.6s infinite;
161
+ }
162
+ @keyframes ping{
163
+ 0%{ box-shadow: 0 0 0 0 rgba(255,209,102,.55); }
164
+ 70%{ box-shadow: 0 0 0 10px rgba(255,209,102,0); }
165
+ 100%{ box-shadow: 0 0 0 0 rgba(255,209,102,0); }
166
+ }
167
+
168
+ h1{
169
+ margin: 0 0 8px;
170
+ font-size: clamp(26px, 3.3vw, 34px);
171
+ letter-spacing: -0.02em;
172
+ line-height: 1.15;
173
+ }
174
+
175
+ p{
176
+ margin: 0;
177
+ color: var(--muted2);
178
+ line-height: 1.55;
179
+ font-size: 15.5px;
180
+ }
181
+
182
+ .hero{
183
+ display:flex;
184
+ align-items:center;
185
+ gap: 14px;
186
+ margin-top: 10px;
187
+ }
188
+
189
+ /* Animated wifi icon */
190
+ .wifi{
191
+ width: 54px; height: 54px;
192
+ border-radius: 16px;
193
+ display:grid; place-items:center;
194
+ background: rgba(255,255,255,.06);
195
+ border:1px solid rgba(255,255,255,.12);
196
+ position:relative;
197
+ overflow:hidden;
198
+ }
199
+ .wifi::after{
200
+ content:"";
201
+ position:absolute; inset:-40%;
202
+ background: radial-gradient(circle at 30% 30%, rgba(255,255,255,.18), transparent 55%);
203
+ animation: sheen 2.8s ease-in-out infinite;
204
+ }
205
+ @keyframes sheen{
206
+ 0%,100%{ transform: translate(-8px,-6px) rotate(0deg); opacity:.75;}
207
+ 50%{ transform: translate(10px,8px) rotate(8deg); opacity:.9;}
208
+ }
209
+
210
+ .wifi svg{ position:relative; z-index:1; opacity:.95; }
211
+ .wifi path{ animation: wave 1.6s ease-in-out infinite; transform-origin:center; }
212
+ .wifi path:nth-child(1){ opacity:.35; animation-delay:.05s;}
213
+ .wifi path:nth-child(2){ opacity:.55; animation-delay:.12s;}
214
+ .wifi path:nth-child(3){ opacity:.75; animation-delay:.2s;}
215
+ .wifi circle{ opacity:.9; animation: blink 1.2s ease-in-out infinite; }
216
+
217
+ @keyframes wave{
218
+ 0%,100%{ transform: scale(1); }
219
+ 50%{ transform: scale(1.06); }
220
+ }
221
+ @keyframes blink{
222
+ 0%,100%{ opacity:.9; }
223
+ 50%{ opacity:.45; }
224
+ }
225
+
226
+ .actions{
227
+ display:flex;
228
+ flex-wrap:wrap;
229
+ gap: 10px;
230
+ margin-top: 18px;
231
+ }
232
+
233
+ .btn{
234
+ appearance:none;
235
+ border:0;
236
+ border-radius: 14px;
237
+ padding: 11px 14px;
238
+ cursor:pointer;
239
+ text-decoration:none;
240
+ display:inline-flex;
241
+ align-items:center;
242
+ gap:10px;
243
+ font-weight: 650;
244
+ font-size: 14.5px;
245
+ letter-spacing: .01em;
246
+ transition: transform .12s ease, filter .12s ease, background .2s ease, border-color .2s ease;
247
+ user-select:none;
248
+ }
249
+ .btn:active{ transform: translateY(1px) scale(.99); }
250
+
251
+ .primary{
252
+ background: #ffffff;
253
+ color: #0b0f19;
254
+ box-shadow: 0 8px 24px rgba(255,255,255,.12);
255
+ }
256
+ .primary:hover{ filter: brightness(0.98); }
257
+
258
+ .ghost{
259
+ background: rgba(255,255,255,.08);
260
+ color: var(--text);
261
+ border: 1px solid rgba(255,255,255,.14);
262
+ }
263
+ .ghost:hover{
264
+ background: rgba(255,255,255,.10);
265
+ border-color: rgba(255,255,255,.18);
266
+ }
267
+
268
+ .hint{
269
+ margin-top: 14px;
270
+ display:flex;
271
+ gap:10px;
272
+ flex-wrap:wrap;
273
+ align-items:center;
274
+ color: var(--muted2);
275
+ font-size: 13px;
276
+ }
277
+
278
+ .kbd{
279
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
280
+ font-size: 12px;
281
+ padding: 4px 8px;
282
+ border-radius: 9px;
283
+ border:1px solid rgba(255,255,255,.14);
284
+ background: rgba(0,0,0,.20);
285
+ color: rgba(255,255,255,.82);
286
+ }
287
+
288
+ /* Toast */
289
+ .toast{
290
+ position: fixed;
291
+ left: 50%;
292
+ bottom: 18px;
293
+ transform: translateX(-50%);
294
+ padding: 10px 12px;
295
+ border-radius: 999px;
296
+ background: rgba(0,0,0,.40);
297
+ border:1px solid rgba(255,255,255,.14);
298
+ color: rgba(255,255,255,.86);
299
+ font-size: 13px;
300
+ display:flex;
301
+ gap:10px;
302
+ align-items:center;
303
+ backdrop-filter: blur(10px);
304
+ -webkit-backdrop-filter: blur(10px);
305
+ box-shadow: 0 10px 40px rgba(0,0,0,.35);
306
+ opacity:0;
307
+ pointer-events:none;
308
+ transition: opacity .22s ease, transform .22s ease;
309
+ }
310
+ .toast.show{
311
+ opacity:1;
312
+ transform: translateX(-50%) translateY(-2px);
313
+ }
314
+ .toast .okdot{
315
+ width:10px;height:10px;border-radius:50%;
316
+ background: var(--good);
317
+ box-shadow: 0 0 0 6px rgba(46,229,157,.10);
318
+ }
319
+
320
+ /* Reduce motion */
321
+ @media (prefers-reduced-motion: reduce){
322
+ .blob, .dot, .wifi path, .wifi circle, .card{ animation:none !important; }
323
+ .toast{ transition:none; }
324
+ }
62
325
  </style>
63
326
  </head>
327
+
64
328
  <body>
65
- <div class="box">
66
- <h1>\u26A1 You\u2019re Offline</h1>
67
- <p>No internet connection detected. You can retry, or go back to the homepage (if cached).</p>
68
- <div class="row">
69
- <button onclick="location.reload()">Retry</button>
70
- <a href="/">Go Home</a>
71
- </div>
329
+ <div class="blob b1"></div>
330
+ <div class="blob b2"></div>
331
+ <div class="blob b3"></div>
332
+
333
+ <main class="wrap" role="main">
334
+ <section class="card" aria-labelledby="offline-title">
335
+ <div class="top">
336
+ <div>
337
+ <h1 id="offline-title">\u{1F635}\u200D\u{1F4AB} You\u2019re Offline</h1>
338
+ <p>
339
+ No internet connection detected. Don\u2019t worry \u2014 you can retry now,
340
+ or go home if it\u2019s already cached. \u2728
341
+ </p>
342
+ </div>
343
+
344
+ <div class="badge" aria-live="polite">
345
+ <span class="dot" aria-hidden="true"></span>
346
+ <span id="netText">Offline mode</span>
347
+ </div>
348
+ </div>
349
+
350
+ <div class="hero">
351
+ <div class="wifi" aria-hidden="true">
352
+ <svg width="30" height="30" viewBox="0 0 24 24" fill="none">
353
+ <path d="M2.5 9.2C8.6 3.2 15.4 3.2 21.5 9.2" stroke="white" stroke-width="2" stroke-linecap="round"/>
354
+ <path d="M5.8 12.4C10.1 8.3 13.9 8.3 18.2 12.4" stroke="white" stroke-width="2" stroke-linecap="round"/>
355
+ <path d="M9.2 15.7C11.2 13.9 12.8 13.9 14.8 15.7" stroke="white" stroke-width="2" stroke-linecap="round"/>
356
+ <circle cx="12" cy="19" r="1.4" fill="white"/>
357
+ </svg>
358
+ </div>
359
+
360
+ <div>
361
+ <p style="margin:0 0 6px;color:rgba(255,255,255,.86);font-weight:650">
362
+ Tip: Your cached pages may still open \u26A1
363
+ </p>
364
+ <p style="margin:0;color:rgba(255,255,255,.62)">
365
+ We\u2019ll auto-refresh when you\u2019re back online \u2705
366
+ </p>
367
+ </div>
368
+ </div>
369
+
370
+ <div class="actions">
371
+ <button class="btn primary" id="retryBtn" type="button">
372
+ \u{1F504} Retry
373
+ </button>
374
+ <a class="btn ghost" href="/" rel="noopener">
375
+ \u{1F3E0} Go Home
376
+ </a>
377
+ <button class="btn ghost" id="copyBtn" type="button" title="Copy current URL">
378
+ \u{1F517} Copy URL
379
+ </button>
380
+ </div>
381
+
382
+ <div class="hint">
383
+ <span>Quick keys:</span>
384
+ <span class="kbd">R</span> Retry
385
+ <span class="kbd">H</span> Home
386
+ </div>
387
+ </section>
388
+ </main>
389
+
390
+ <div class="toast" id="toast" role="status" aria-live="polite">
391
+ <span class="okdot" aria-hidden="true"></span>
392
+ <span id="toastText">Back online \u2014 reloading\u2026</span>
72
393
  </div>
394
+
395
+ <script>
396
+ const retryBtn = document.getElementById("retryBtn");
397
+ const copyBtn = document.getElementById("copyBtn");
398
+ const netText = document.getElementById("netText");
399
+ const toast = document.getElementById("toast");
400
+ const toastText= document.getElementById("toastText");
401
+
402
+ function showToast(msg){
403
+ toastText.textContent = msg;
404
+ toast.classList.add("show");
405
+ clearTimeout(showToast._t);
406
+ showToast._t = setTimeout(() => toast.classList.remove("show"), 2200);
407
+ }
408
+
409
+ function updateStatus(){
410
+ const online = navigator.onLine;
411
+ netText.textContent = online ? "Online" : "Offline mode";
412
+ // If it becomes online, reload after tiny delay (feels smoother)
413
+ if (online){
414
+ showToast("\u2705 Back online \u2014 reloading\u2026");
415
+ setTimeout(() => location.reload(), 650);
416
+ }
417
+ }
418
+
419
+ retryBtn.addEventListener("click", () => location.reload());
420
+
421
+ copyBtn.addEventListener("click", async () => {
422
+ try{
423
+ await navigator.clipboard.writeText(location.href);
424
+ showToast("\u{1F4CB} URL copied!");
425
+ }catch{
426
+ // fallback
427
+ const ta = document.createElement("textarea");
428
+ ta.value = location.href;
429
+ document.body.appendChild(ta);
430
+ ta.select();
431
+ document.execCommand("copy");
432
+ ta.remove();
433
+ showToast("\u{1F4CB} URL copied!");
434
+ }
435
+ });
436
+
437
+ window.addEventListener("online", updateStatus);
438
+ window.addEventListener("offline", updateStatus);
439
+
440
+ // Keyboard shortcuts
441
+ window.addEventListener("keydown", (e) => {
442
+ const k = (e.key || "").toLowerCase();
443
+ if (k === "r") location.reload();
444
+ if (k === "h") location.href = "/";
445
+ });
446
+
447
+ // initial
448
+ updateStatus();
449
+ </script>
73
450
  </body>
74
451
  </html>`;
75
452
  }
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts","../src/core/utils.ts","../src/core/offlineHtml.ts","../src/core/offlineSvg.ts","../src/core/swTemplate.ts"],"sourcesContent":["#!/usr/bin/env node\r\nimport path from \"node:path\";\r\nimport { parseArgs, splitList, writeFileSafe, exists } from \"./core/utils\";\r\nimport { offlineHtmlTemplate } from \"./core/offlineHtml\";\r\nimport { offlineSvgTemplate } from \"./core/offlineSvg\";\r\nimport { buildServiceWorkerJS } from \"./core/swTemplate\";\r\nimport type { OfflineKitBuildOptions } from \"./types\";\r\n\r\nfunction normalizePublicPath(p: string) {\r\n if (!p.startsWith(\"/\")) return \"/\" + p;\r\n return p;\r\n}\r\n\r\nfunction withDefaults(o: OfflineKitBuildOptions): Required<OfflineKitBuildOptions> {\r\n return {\r\n outDir: o.outDir ?? \"public\",\r\n swFileName: o.swFileName ?? \"sw.js\",\r\n offlinePage: normalizePublicPath(o.offlinePage ?? \"/offline.html\"),\r\n offlineImage: normalizePublicPath(o.offlineImage ?? \"/offline.svg\"),\r\n cacheName: o.cacheName ?? \"offline-page-kit\",\r\n precache: o.precache ?? [],\r\n htmlStrategy: o.htmlStrategy ?? \"networkFirst\",\r\n assetStrategy: o.assetStrategy ?? \"staleWhileRevalidate\",\r\n imageStrategy: o.imageStrategy ?? \"cacheFirst\",\r\n assetExtensions: o.assetExtensions ?? [],\r\n apiPrefixes: o.apiPrefixes ?? [],\r\n };\r\n}\r\n\r\nconst args = parseArgs(process.argv.slice(2));\r\nconst cmd = process.argv.slice(2).find(a => !a.startsWith(\"--\")) || \"init\";\r\n\r\nconst outDir = path.resolve(process.cwd(), args.get(\"outDir\") || \"public\");\r\n\r\nconst options = withDefaults({\r\n outDir,\r\n swFileName: args.get(\"swFileName\") || \"sw.js\",\r\n offlinePage: args.get(\"offlinePage\") || \"/offline.html\",\r\n offlineImage: args.get(\"offlineImage\") || \"/offline.svg\",\r\n cacheName: args.get(\"cacheName\") || \"offline-page-kit\",\r\n precache: splitList(args.get(\"precache\")),\r\n htmlStrategy: (args.get(\"htmlStrategy\") as any) || \"networkFirst\",\r\n assetStrategy: (args.get(\"assetStrategy\") as any) || \"staleWhileRevalidate\",\r\n imageStrategy: (args.get(\"imageStrategy\") as any) || \"cacheFirst\",\r\n assetExtensions: splitList(args.get(\"assetExtensions\")),\r\n apiPrefixes: splitList(args.get(\"apiPrefixes\")),\r\n} as OfflineKitBuildOptions);\r\n\r\nconst swOut = path.join(outDir, options.swFileName);\r\nconst offlineHtmlOut = path.join(outDir, options.offlinePage.replace(/^\\//, \"\"));\r\nconst offlineSvgOut = path.join(outDir, options.offlineImage.replace(/^\\//, \"\"));\r\n\r\nif (cmd === \"init\" || cmd === \"build\") {\r\n // generate offline page if missing OR force (when build)\r\n if (cmd === \"build\" || !exists(offlineHtmlOut)) {\r\n writeFileSafe(offlineHtmlOut, offlineHtmlTemplate());\r\n }\r\n if (cmd === \"build\" || !exists(offlineSvgOut)) {\r\n writeFileSafe(offlineSvgOut, offlineSvgTemplate());\r\n }\r\n\r\n const sw = buildServiceWorkerJS(options);\r\n writeFileSafe(swOut, sw);\r\n\r\n console.log(`[offline-page-kit] Generated:\r\n- ${swOut}\r\n- ${offlineHtmlOut}\r\n- ${offlineSvgOut}\r\n`);\r\n} else {\r\n console.log(`[offline-page-kit] Unknown command: ${cmd}\r\nUse:\r\n offline-page-kit init --outDir public\r\n offline-page-kit build --outDir public\r\n`);\r\n}","import fs from \"node:fs\";\r\nimport path from \"node:path\";\r\n\r\nexport function ensureDir(p: string) {\r\n fs.mkdirSync(p, { recursive: true });\r\n}\r\n\r\nexport function writeFileSafe(filePath: string, content: string) {\r\n ensureDir(path.dirname(filePath));\r\n fs.writeFileSync(filePath, content, \"utf8\");\r\n}\r\n\r\nexport function exists(filePath: string) {\r\n try { fs.accessSync(filePath); return true; } catch { return false; }\r\n}\r\n\r\nexport function parseArgs(argv: string[]) {\r\n const m = new Map<string, string>();\r\n\r\n const norm = (k: string) => k.replace(/-/g, \"\").toLowerCase();\r\n\r\n for (let i = 0; i < argv.length; i++) {\r\n const a = argv[i];\r\n if (!a.startsWith(\"--\")) continue;\r\n\r\n const rawKey = a.slice(2);\r\n const key = norm(rawKey);\r\n\r\n const value = argv[i + 1] && !argv[i + 1].startsWith(\"--\") ? argv[++i] : \"true\";\r\n m.set(key, value);\r\n }\r\n\r\n return {\r\n get(name: string) {\r\n return m.get(norm(name));\r\n }\r\n } as unknown as Map<string, string>;\r\n}\r\n\r\nexport function splitList(v: string | undefined) {\r\n return (v || \"\")\r\n .split(\",\")\r\n .map(s => s.trim())\r\n .filter(Boolean);\r\n}","export function offlineHtmlTemplate(title = \"You're Offline\") {\r\n return `<!doctype html>\r\n<html lang=\"en\">\r\n<head>\r\n <meta charset=\"UTF-8\"/>\r\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"/>\r\n <title>${title}</title>\r\n <style>\r\n body{margin:0;height:100vh;display:grid;place-items:center;font-family:system-ui,-apple-system,Segoe UI,Roboto;background:#0b0f19;color:#fff}\r\n .box{max-width:560px;padding:28px 26px;border-radius:18px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.04)}\r\n h1{margin:0 0 10px;font-size:28px}\r\n p{margin:0 0 18px;opacity:.85;line-height:1.5}\r\n .row{display:flex;gap:10px;flex-wrap:wrap}\r\n button,a{appearance:none;border:0;border-radius:12px;padding:10px 14px;cursor:pointer;text-decoration:none}\r\n button{background:#fff;color:#111}\r\n a{background:rgba(255,255,255,.1);color:#fff}\r\n </style>\r\n</head>\r\n<body>\r\n <div class=\"box\">\r\n <h1>⚡ You’re Offline</h1>\r\n <p>No internet connection detected. You can retry, or go back to the homepage (if cached).</p>\r\n <div class=\"row\">\r\n <button onclick=\"location.reload()\">Retry</button>\r\n <a href=\"/\">Go Home</a>\r\n </div>\r\n </div>\r\n</body>\r\n</html>`;\r\n}","export function offlineSvgTemplate() {\r\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1200\" height=\"630\" viewBox=\"0 0 1200 630\">\r\n <rect width=\"1200\" height=\"630\" fill=\"#0b0f19\"/>\r\n <text x=\"80\" y=\"220\" fill=\"#ffffff\" font-size=\"64\" font-family=\"system-ui, -apple-system, Segoe UI, Roboto\">You’re Offline</text>\r\n <text x=\"80\" y=\"300\" fill=\"#cbd5e1\" font-size=\"28\" font-family=\"system-ui, -apple-system, Segoe UI, Roboto\">Please check your connection and try again.</text>\r\n <circle cx=\"1040\" cy=\"220\" r=\"90\" fill=\"rgba(255,255,255,0.08)\"/>\r\n <path d=\"M980 220c40-40 120-40 160 0\" stroke=\"#fff\" stroke-width=\"10\" fill=\"none\" opacity=\"0.6\"/>\r\n <path d=\"M1010 250c25-25 75-25 100 0\" stroke=\"#fff\" stroke-width=\"10\" fill=\"none\" opacity=\"0.6\"/>\r\n <circle cx=\"1060\" cy=\"290\" r=\"10\" fill=\"#fff\" opacity=\"0.7\"/>\r\n</svg>`;\r\n}","import type { OfflineKitBuildOptions } from \"../types\";\r\n\r\nconst js = (v: unknown) => JSON.stringify(v);\r\n\r\nexport function buildServiceWorkerJS(options: Required<OfflineKitBuildOptions>) {\r\n const { cacheName, offlinePage, offlineImage } = options;\r\n\r\n return `/* offline-page-kit service worker (minimal) */\r\nconst CACHE_NAME = ${js(cacheName)};\r\nconst OFFLINE_PAGE = ${js(offlinePage)};\r\nconst OFFLINE_IMAGE = ${js(offlineImage)};\r\n\r\nself.addEventListener(\"install\", (event) => {\r\n event.waitUntil((async () => {\r\n const cache = await caches.open(CACHE_NAME);\r\n\r\n // ✅ Do not let one 404 kill the install\r\n await Promise.allSettled([\r\n cache.add(OFFLINE_PAGE),\r\n cache.add(OFFLINE_IMAGE),\r\n ]);\r\n\r\n await self.skipWaiting();\r\n })());\r\n});\r\n\r\nself.addEventListener(\"activate\", (event) => {\r\n event.waitUntil((async () => {\r\n await self.clients.claim();\r\n })());\r\n});\r\n\r\nself.addEventListener(\"fetch\", (event) => {\r\n const req = event.request;\r\n\r\n // ✅ Offline fallback only for page navigations\r\n if (req.mode === \"navigate\") {\r\n event.respondWith((async () => {\r\n try {\r\n return await fetch(req);\r\n } catch {\r\n const cache = await caches.open(CACHE_NAME);\r\n return (await cache.match(OFFLINE_PAGE)) || new Response(\"Offline\", { status: 503 });\r\n }\r\n })());\r\n }\r\n});\r\n`;\r\n}"],"mappings":"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","names":["path","options","path"]}
1
+ {"version":3,"sources":["../src/cli.ts","../src/core/utils.ts","../src/core/offlineHtml.ts","../src/core/offlineSvg.ts","../src/core/swTemplate.ts"],"sourcesContent":["#!/usr/bin/env node\r\nimport path from \"node:path\";\r\nimport { parseArgs, splitList, writeFileSafe, exists } from \"./core/utils\";\r\nimport { offlineHtmlTemplate } from \"./core/offlineHtml\";\r\nimport { offlineSvgTemplate } from \"./core/offlineSvg\";\r\nimport { buildServiceWorkerJS } from \"./core/swTemplate\";\r\nimport type { OfflineKitBuildOptions } from \"./types\";\r\n\r\nfunction normalizePublicPath(p: string) {\r\n if (!p.startsWith(\"/\")) return \"/\" + p;\r\n return p;\r\n}\r\n\r\nfunction withDefaults(o: OfflineKitBuildOptions): Required<OfflineKitBuildOptions> {\r\n return {\r\n outDir: o.outDir ?? \"public\",\r\n swFileName: o.swFileName ?? \"sw.js\",\r\n offlinePage: normalizePublicPath(o.offlinePage ?? \"/offline.html\"),\r\n offlineImage: normalizePublicPath(o.offlineImage ?? \"/offline.svg\"),\r\n cacheName: o.cacheName ?? \"offline-page-kit\",\r\n precache: o.precache ?? [],\r\n htmlStrategy: o.htmlStrategy ?? \"networkFirst\",\r\n assetStrategy: o.assetStrategy ?? \"staleWhileRevalidate\",\r\n imageStrategy: o.imageStrategy ?? \"cacheFirst\",\r\n assetExtensions: o.assetExtensions ?? [],\r\n apiPrefixes: o.apiPrefixes ?? [],\r\n };\r\n}\r\n\r\nconst args = parseArgs(process.argv.slice(2));\r\nconst cmd = process.argv.slice(2).find(a => !a.startsWith(\"--\")) || \"init\";\r\n\r\nconst outDir = path.resolve(process.cwd(), args.get(\"outDir\") || \"public\");\r\n\r\nconst options = withDefaults({\r\n outDir,\r\n swFileName: args.get(\"swFileName\") || \"sw.js\",\r\n offlinePage: args.get(\"offlinePage\") || \"/offline.html\",\r\n offlineImage: args.get(\"offlineImage\") || \"/offline.svg\",\r\n cacheName: args.get(\"cacheName\") || \"offline-page-kit\",\r\n precache: splitList(args.get(\"precache\")),\r\n htmlStrategy: (args.get(\"htmlStrategy\") as any) || \"networkFirst\",\r\n assetStrategy: (args.get(\"assetStrategy\") as any) || \"staleWhileRevalidate\",\r\n imageStrategy: (args.get(\"imageStrategy\") as any) || \"cacheFirst\",\r\n assetExtensions: splitList(args.get(\"assetExtensions\")),\r\n apiPrefixes: splitList(args.get(\"apiPrefixes\")),\r\n} as OfflineKitBuildOptions);\r\n\r\nconst swOut = path.join(outDir, options.swFileName);\r\nconst offlineHtmlOut = path.join(outDir, options.offlinePage.replace(/^\\//, \"\"));\r\nconst offlineSvgOut = path.join(outDir, options.offlineImage.replace(/^\\//, \"\"));\r\n\r\nif (cmd === \"init\" || cmd === \"build\") {\r\n // generate offline page if missing OR force (when build)\r\n if (cmd === \"build\" || !exists(offlineHtmlOut)) {\r\n writeFileSafe(offlineHtmlOut, offlineHtmlTemplate());\r\n }\r\n if (cmd === \"build\" || !exists(offlineSvgOut)) {\r\n writeFileSafe(offlineSvgOut, offlineSvgTemplate());\r\n }\r\n\r\n const sw = buildServiceWorkerJS(options);\r\n writeFileSafe(swOut, sw);\r\n\r\n console.log(`[offline-page-kit] Generated:\r\n- ${swOut}\r\n- ${offlineHtmlOut}\r\n- ${offlineSvgOut}\r\n`);\r\n} else {\r\n console.log(`[offline-page-kit] Unknown command: ${cmd}\r\nUse:\r\n offline-page-kit init --outDir public\r\n offline-page-kit build --outDir public\r\n`);\r\n}","import fs from \"node:fs\";\r\nimport path from \"node:path\";\r\n\r\nexport function ensureDir(p: string) {\r\n fs.mkdirSync(p, { recursive: true });\r\n}\r\n\r\nexport function writeFileSafe(filePath: string, content: string) {\r\n ensureDir(path.dirname(filePath));\r\n fs.writeFileSync(filePath, content, \"utf8\");\r\n}\r\n\r\nexport function exists(filePath: string) {\r\n try { fs.accessSync(filePath); return true; } catch { return false; }\r\n}\r\n\r\nexport function parseArgs(argv: string[]) {\r\n const m = new Map<string, string>();\r\n\r\n const norm = (k: string) => k.replace(/-/g, \"\").toLowerCase();\r\n\r\n for (let i = 0; i < argv.length; i++) {\r\n const a = argv[i];\r\n if (!a.startsWith(\"--\")) continue;\r\n\r\n const rawKey = a.slice(2);\r\n const key = norm(rawKey);\r\n\r\n const value = argv[i + 1] && !argv[i + 1].startsWith(\"--\") ? argv[++i] : \"true\";\r\n m.set(key, value);\r\n }\r\n\r\n return {\r\n get(name: string) {\r\n return m.get(norm(name));\r\n }\r\n } as unknown as Map<string, string>;\r\n}\r\n\r\nexport function splitList(v: string | undefined) {\r\n return (v || \"\")\r\n .split(\",\")\r\n .map(s => s.trim())\r\n .filter(Boolean);\r\n}","export function offlineHtmlTemplate(title = \"You're Offline\") {\r\n return `<!doctype html>\r\n<html lang=\"en\">\r\n<head>\r\n <meta charset=\"UTF-8\"/>\r\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"/>\r\n <title>${title}</title>\r\n <meta name=\"theme-color\" content=\"#0b0f19\"/>\r\n\r\n <style>\r\n :root{\r\n --bg0:#060912;\r\n --bg1:#0b1022;\r\n --card: rgba(255,255,255,.06);\r\n --card2: rgba(255,255,255,.04);\r\n --stroke: rgba(255,255,255,.14);\r\n --text:#ffffff;\r\n --muted: rgba(255,255,255,.78);\r\n --muted2: rgba(255,255,255,.62);\r\n --good:#2ee59d;\r\n --warn:#ffd166;\r\n --shadow: 0 20px 80px rgba(0,0,0,.55);\r\n --radius: 22px;\r\n }\r\n\r\n *{box-sizing:border-box}\r\n html,body{height:100%}\r\n body{\r\n margin:0;\r\n font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, \"Apple Color Emoji\",\"Segoe UI Emoji\";\r\n color:var(--text);\r\n background:\r\n radial-gradient(900px 500px at 18% 18%, rgba(120,140,255,.20), transparent 60%),\r\n radial-gradient(700px 500px at 82% 20%, rgba(46,229,157,.16), transparent 58%),\r\n radial-gradient(600px 520px at 50% 95%, rgba(255,209,102,.14), transparent 55%),\r\n linear-gradient(180deg, var(--bg0), var(--bg1));\r\n overflow:hidden;\r\n }\r\n\r\n /* Floating blobs (subtle animation) */\r\n .blob{\r\n position:absolute; inset:auto;\r\n width: 520px; height: 520px;\r\n border-radius: 50%;\r\n filter: blur(42px);\r\n opacity:.35;\r\n animation: float 10s ease-in-out infinite;\r\n pointer-events:none;\r\n transform: translate3d(0,0,0);\r\n }\r\n .blob.b1{left:-180px; top:-200px; background: rgba(120,140,255,.55); animation-duration: 12s;}\r\n .blob.b2{right:-210px; top:-150px; background: rgba(46,229,157,.52); animation-duration: 14s;}\r\n .blob.b3{left:18%; bottom:-320px; background: rgba(255,209,102,.45); animation-duration: 16s;}\r\n\r\n @keyframes float{\r\n 0%,100%{ transform: translate(0,0) scale(1);}\r\n 50%{ transform: translate(22px,-18px) scale(1.06);}\r\n }\r\n\r\n /* Main layout */\r\n .wrap{\r\n min-height:100%;\r\n display:grid;\r\n place-items:center;\r\n padding: 24px;\r\n position:relative;\r\n z-index:1;\r\n }\r\n\r\n .card{\r\n width:min(720px, 100%);\r\n border-radius: var(--radius);\r\n background: linear-gradient(180deg, var(--card), var(--card2));\r\n border:1px solid var(--stroke);\r\n box-shadow: var(--shadow);\r\n padding: 26px;\r\n backdrop-filter: blur(10px);\r\n -webkit-backdrop-filter: blur(10px);\r\n\r\n animation: pop .55s cubic-bezier(.2,.9,.2,1) both;\r\n }\r\n\r\n @keyframes pop{\r\n from{ opacity:0; transform: translateY(14px) scale(.98); }\r\n to{ opacity:1; transform: translateY(0) scale(1); }\r\n }\r\n\r\n .top{\r\n display:flex;\r\n align-items:flex-start;\r\n justify-content:space-between;\r\n gap:16px;\r\n margin-bottom: 14px;\r\n }\r\n\r\n .badge{\r\n display:inline-flex;\r\n align-items:center;\r\n gap:10px;\r\n padding: 10px 12px;\r\n border-radius: 999px;\r\n border:1px solid rgba(255,255,255,.14);\r\n background: rgba(0,0,0,.18);\r\n color: var(--muted);\r\n font-size: 13px;\r\n user-select:none;\r\n white-space:nowrap;\r\n }\r\n\r\n /* Ping dot */\r\n .dot{\r\n width:10px;height:10px;border-radius:50%;\r\n background: var(--warn);\r\n box-shadow: 0 0 0 0 rgba(255,209,102,.65);\r\n animation: ping 1.6s infinite;\r\n }\r\n @keyframes ping{\r\n 0%{ box-shadow: 0 0 0 0 rgba(255,209,102,.55); }\r\n 70%{ box-shadow: 0 0 0 10px rgba(255,209,102,0); }\r\n 100%{ box-shadow: 0 0 0 0 rgba(255,209,102,0); }\r\n }\r\n\r\n h1{\r\n margin: 0 0 8px;\r\n font-size: clamp(26px, 3.3vw, 34px);\r\n letter-spacing: -0.02em;\r\n line-height: 1.15;\r\n }\r\n\r\n p{\r\n margin: 0;\r\n color: var(--muted2);\r\n line-height: 1.55;\r\n font-size: 15.5px;\r\n }\r\n\r\n .hero{\r\n display:flex;\r\n align-items:center;\r\n gap: 14px;\r\n margin-top: 10px;\r\n }\r\n\r\n /* Animated wifi icon */\r\n .wifi{\r\n width: 54px; height: 54px;\r\n border-radius: 16px;\r\n display:grid; place-items:center;\r\n background: rgba(255,255,255,.06);\r\n border:1px solid rgba(255,255,255,.12);\r\n position:relative;\r\n overflow:hidden;\r\n }\r\n .wifi::after{\r\n content:\"\";\r\n position:absolute; inset:-40%;\r\n background: radial-gradient(circle at 30% 30%, rgba(255,255,255,.18), transparent 55%);\r\n animation: sheen 2.8s ease-in-out infinite;\r\n }\r\n @keyframes sheen{\r\n 0%,100%{ transform: translate(-8px,-6px) rotate(0deg); opacity:.75;}\r\n 50%{ transform: translate(10px,8px) rotate(8deg); opacity:.9;}\r\n }\r\n\r\n .wifi svg{ position:relative; z-index:1; opacity:.95; }\r\n .wifi path{ animation: wave 1.6s ease-in-out infinite; transform-origin:center; }\r\n .wifi path:nth-child(1){ opacity:.35; animation-delay:.05s;}\r\n .wifi path:nth-child(2){ opacity:.55; animation-delay:.12s;}\r\n .wifi path:nth-child(3){ opacity:.75; animation-delay:.2s;}\r\n .wifi circle{ opacity:.9; animation: blink 1.2s ease-in-out infinite; }\r\n\r\n @keyframes wave{\r\n 0%,100%{ transform: scale(1); }\r\n 50%{ transform: scale(1.06); }\r\n }\r\n @keyframes blink{\r\n 0%,100%{ opacity:.9; }\r\n 50%{ opacity:.45; }\r\n }\r\n\r\n .actions{\r\n display:flex;\r\n flex-wrap:wrap;\r\n gap: 10px;\r\n margin-top: 18px;\r\n }\r\n\r\n .btn{\r\n appearance:none;\r\n border:0;\r\n border-radius: 14px;\r\n padding: 11px 14px;\r\n cursor:pointer;\r\n text-decoration:none;\r\n display:inline-flex;\r\n align-items:center;\r\n gap:10px;\r\n font-weight: 650;\r\n font-size: 14.5px;\r\n letter-spacing: .01em;\r\n transition: transform .12s ease, filter .12s ease, background .2s ease, border-color .2s ease;\r\n user-select:none;\r\n }\r\n .btn:active{ transform: translateY(1px) scale(.99); }\r\n\r\n .primary{\r\n background: #ffffff;\r\n color: #0b0f19;\r\n box-shadow: 0 8px 24px rgba(255,255,255,.12);\r\n }\r\n .primary:hover{ filter: brightness(0.98); }\r\n\r\n .ghost{\r\n background: rgba(255,255,255,.08);\r\n color: var(--text);\r\n border: 1px solid rgba(255,255,255,.14);\r\n }\r\n .ghost:hover{\r\n background: rgba(255,255,255,.10);\r\n border-color: rgba(255,255,255,.18);\r\n }\r\n\r\n .hint{\r\n margin-top: 14px;\r\n display:flex;\r\n gap:10px;\r\n flex-wrap:wrap;\r\n align-items:center;\r\n color: var(--muted2);\r\n font-size: 13px;\r\n }\r\n\r\n .kbd{\r\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", monospace;\r\n font-size: 12px;\r\n padding: 4px 8px;\r\n border-radius: 9px;\r\n border:1px solid rgba(255,255,255,.14);\r\n background: rgba(0,0,0,.20);\r\n color: rgba(255,255,255,.82);\r\n }\r\n\r\n /* Toast */\r\n .toast{\r\n position: fixed;\r\n left: 50%;\r\n bottom: 18px;\r\n transform: translateX(-50%);\r\n padding: 10px 12px;\r\n border-radius: 999px;\r\n background: rgba(0,0,0,.40);\r\n border:1px solid rgba(255,255,255,.14);\r\n color: rgba(255,255,255,.86);\r\n font-size: 13px;\r\n display:flex;\r\n gap:10px;\r\n align-items:center;\r\n backdrop-filter: blur(10px);\r\n -webkit-backdrop-filter: blur(10px);\r\n box-shadow: 0 10px 40px rgba(0,0,0,.35);\r\n opacity:0;\r\n pointer-events:none;\r\n transition: opacity .22s ease, transform .22s ease;\r\n }\r\n .toast.show{\r\n opacity:1;\r\n transform: translateX(-50%) translateY(-2px);\r\n }\r\n .toast .okdot{\r\n width:10px;height:10px;border-radius:50%;\r\n background: var(--good);\r\n box-shadow: 0 0 0 6px rgba(46,229,157,.10);\r\n }\r\n\r\n /* Reduce motion */\r\n @media (prefers-reduced-motion: reduce){\r\n .blob, .dot, .wifi path, .wifi circle, .card{ animation:none !important; }\r\n .toast{ transition:none; }\r\n }\r\n </style>\r\n</head>\r\n\r\n<body>\r\n <div class=\"blob b1\"></div>\r\n <div class=\"blob b2\"></div>\r\n <div class=\"blob b3\"></div>\r\n\r\n <main class=\"wrap\" role=\"main\">\r\n <section class=\"card\" aria-labelledby=\"offline-title\">\r\n <div class=\"top\">\r\n <div>\r\n <h1 id=\"offline-title\">😵‍💫 You’re Offline</h1>\r\n <p>\r\n No internet connection detected. Don’t worry — you can retry now,\r\n or go home if it’s already cached. ✨\r\n </p>\r\n </div>\r\n\r\n <div class=\"badge\" aria-live=\"polite\">\r\n <span class=\"dot\" aria-hidden=\"true\"></span>\r\n <span id=\"netText\">Offline mode</span>\r\n </div>\r\n </div>\r\n\r\n <div class=\"hero\">\r\n <div class=\"wifi\" aria-hidden=\"true\">\r\n <svg width=\"30\" height=\"30\" viewBox=\"0 0 24 24\" fill=\"none\">\r\n <path d=\"M2.5 9.2C8.6 3.2 15.4 3.2 21.5 9.2\" stroke=\"white\" stroke-width=\"2\" stroke-linecap=\"round\"/>\r\n <path d=\"M5.8 12.4C10.1 8.3 13.9 8.3 18.2 12.4\" stroke=\"white\" stroke-width=\"2\" stroke-linecap=\"round\"/>\r\n <path d=\"M9.2 15.7C11.2 13.9 12.8 13.9 14.8 15.7\" stroke=\"white\" stroke-width=\"2\" stroke-linecap=\"round\"/>\r\n <circle cx=\"12\" cy=\"19\" r=\"1.4\" fill=\"white\"/>\r\n </svg>\r\n </div>\r\n\r\n <div>\r\n <p style=\"margin:0 0 6px;color:rgba(255,255,255,.86);font-weight:650\">\r\n Tip: Your cached pages may still open ⚡\r\n </p>\r\n <p style=\"margin:0;color:rgba(255,255,255,.62)\">\r\n We’ll auto-refresh when you’re back online ✅\r\n </p>\r\n </div>\r\n </div>\r\n\r\n <div class=\"actions\">\r\n <button class=\"btn primary\" id=\"retryBtn\" type=\"button\">\r\n 🔄 Retry\r\n </button>\r\n <a class=\"btn ghost\" href=\"/\" rel=\"noopener\">\r\n 🏠 Go Home\r\n </a>\r\n <button class=\"btn ghost\" id=\"copyBtn\" type=\"button\" title=\"Copy current URL\">\r\n 🔗 Copy URL\r\n </button>\r\n </div>\r\n\r\n <div class=\"hint\">\r\n <span>Quick keys:</span>\r\n <span class=\"kbd\">R</span> Retry\r\n <span class=\"kbd\">H</span> Home\r\n </div>\r\n </section>\r\n </main>\r\n\r\n <div class=\"toast\" id=\"toast\" role=\"status\" aria-live=\"polite\">\r\n <span class=\"okdot\" aria-hidden=\"true\"></span>\r\n <span id=\"toastText\">Back online — reloading…</span>\r\n </div>\r\n\r\n <script>\r\n const retryBtn = document.getElementById(\"retryBtn\");\r\n const copyBtn = document.getElementById(\"copyBtn\");\r\n const netText = document.getElementById(\"netText\");\r\n const toast = document.getElementById(\"toast\");\r\n const toastText= document.getElementById(\"toastText\");\r\n\r\n function showToast(msg){\r\n toastText.textContent = msg;\r\n toast.classList.add(\"show\");\r\n clearTimeout(showToast._t);\r\n showToast._t = setTimeout(() => toast.classList.remove(\"show\"), 2200);\r\n }\r\n\r\n function updateStatus(){\r\n const online = navigator.onLine;\r\n netText.textContent = online ? \"Online\" : \"Offline mode\";\r\n // If it becomes online, reload after tiny delay (feels smoother)\r\n if (online){\r\n showToast(\"✅ Back online — reloading…\");\r\n setTimeout(() => location.reload(), 650);\r\n }\r\n }\r\n\r\n retryBtn.addEventListener(\"click\", () => location.reload());\r\n\r\n copyBtn.addEventListener(\"click\", async () => {\r\n try{\r\n await navigator.clipboard.writeText(location.href);\r\n showToast(\"📋 URL copied!\");\r\n }catch{\r\n // fallback\r\n const ta = document.createElement(\"textarea\");\r\n ta.value = location.href;\r\n document.body.appendChild(ta);\r\n ta.select();\r\n document.execCommand(\"copy\");\r\n ta.remove();\r\n showToast(\"📋 URL copied!\");\r\n }\r\n });\r\n\r\n window.addEventListener(\"online\", updateStatus);\r\n window.addEventListener(\"offline\", updateStatus);\r\n\r\n // Keyboard shortcuts\r\n window.addEventListener(\"keydown\", (e) => {\r\n const k = (e.key || \"\").toLowerCase();\r\n if (k === \"r\") location.reload();\r\n if (k === \"h\") location.href = \"/\";\r\n });\r\n\r\n // initial\r\n updateStatus();\r\n </script>\r\n</body>\r\n</html>`;\r\n}","export function offlineSvgTemplate() {\r\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1200\" height=\"630\" viewBox=\"0 0 1200 630\">\r\n <rect width=\"1200\" height=\"630\" fill=\"#0b0f19\"/>\r\n <text x=\"80\" y=\"220\" fill=\"#ffffff\" font-size=\"64\" font-family=\"system-ui, -apple-system, Segoe UI, Roboto\">You’re Offline</text>\r\n <text x=\"80\" y=\"300\" fill=\"#cbd5e1\" font-size=\"28\" font-family=\"system-ui, -apple-system, Segoe UI, Roboto\">Please check your connection and try again.</text>\r\n <circle cx=\"1040\" cy=\"220\" r=\"90\" fill=\"rgba(255,255,255,0.08)\"/>\r\n <path d=\"M980 220c40-40 120-40 160 0\" stroke=\"#fff\" stroke-width=\"10\" fill=\"none\" opacity=\"0.6\"/>\r\n <path d=\"M1010 250c25-25 75-25 100 0\" stroke=\"#fff\" stroke-width=\"10\" fill=\"none\" opacity=\"0.6\"/>\r\n <circle cx=\"1060\" cy=\"290\" r=\"10\" fill=\"#fff\" opacity=\"0.7\"/>\r\n</svg>`;\r\n}","import type { OfflineKitBuildOptions } from \"../types\";\r\n\r\nconst js = (v: unknown) => JSON.stringify(v);\r\n\r\nexport function buildServiceWorkerJS(options: Required<OfflineKitBuildOptions>) {\r\n const { cacheName, offlinePage, offlineImage } = options;\r\n\r\n return `/* offline-page-kit service worker (minimal) */\r\nconst CACHE_NAME = ${js(cacheName)};\r\nconst OFFLINE_PAGE = ${js(offlinePage)};\r\nconst OFFLINE_IMAGE = ${js(offlineImage)};\r\n\r\nself.addEventListener(\"install\", (event) => {\r\n event.waitUntil((async () => {\r\n const cache = await caches.open(CACHE_NAME);\r\n\r\n // ✅ Do not let one 404 kill the install\r\n await Promise.allSettled([\r\n cache.add(OFFLINE_PAGE),\r\n cache.add(OFFLINE_IMAGE),\r\n ]);\r\n\r\n await self.skipWaiting();\r\n })());\r\n});\r\n\r\nself.addEventListener(\"activate\", (event) => {\r\n event.waitUntil((async () => {\r\n await self.clients.claim();\r\n })());\r\n});\r\n\r\nself.addEventListener(\"fetch\", (event) => {\r\n const req = event.request;\r\n\r\n // ✅ Offline fallback only for page navigations\r\n if (req.mode === \"navigate\") {\r\n event.respondWith((async () => {\r\n try {\r\n return await fetch(req);\r\n } catch {\r\n const cache = await caches.open(CACHE_NAME);\r\n return (await cache.match(OFFLINE_PAGE)) || new Response(\"Offline\", { status: 503 });\r\n }\r\n })());\r\n }\r\n});\r\n`;\r\n}"],"mappings":";;;AACA,OAAOA,WAAU;;;ACDjB,OAAO,QAAQ;AACf,OAAO,UAAU;AAEV,SAAS,UAAU,GAAW;AACjC,KAAG,UAAU,GAAG,EAAE,WAAW,KAAK,CAAC;AACvC;AAEO,SAAS,cAAc,UAAkB,SAAiB;AAC7D,YAAU,KAAK,QAAQ,QAAQ,CAAC;AAChC,KAAG,cAAc,UAAU,SAAS,MAAM;AAC9C;AAEO,SAAS,OAAO,UAAkB;AACrC,MAAI;AAAE,OAAG,WAAW,QAAQ;AAAG,WAAO;AAAA,EAAM,QAAQ;AAAE,WAAO;AAAA,EAAO;AACxE;AAEO,SAAS,UAAU,MAAgB;AACtC,QAAM,IAAI,oBAAI,IAAoB;AAElC,QAAM,OAAO,CAAC,MAAc,EAAE,QAAQ,MAAM,EAAE,EAAE,YAAY;AAE5D,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AAClC,UAAM,IAAI,KAAK,CAAC;AAChB,QAAI,CAAC,EAAE,WAAW,IAAI,EAAG;AAEzB,UAAM,SAAS,EAAE,MAAM,CAAC;AACxB,UAAM,MAAM,KAAK,MAAM;AAEvB,UAAM,QAAQ,KAAK,IAAI,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,EAAE,WAAW,IAAI,IAAI,KAAK,EAAE,CAAC,IAAI;AACzE,MAAE,IAAI,KAAK,KAAK;AAAA,EACpB;AAEA,SAAO;AAAA,IACH,IAAI,MAAc;AACd,aAAO,EAAE,IAAI,KAAK,IAAI,CAAC;AAAA,IAC3B;AAAA,EACJ;AACJ;AAEO,SAAS,UAAU,GAAuB;AAC7C,UAAQ,KAAK,IACR,MAAM,GAAG,EACT,IAAI,OAAK,EAAE,KAAK,CAAC,EACjB,OAAO,OAAO;AACvB;;;AC5CO,SAAS,oBAAoB,QAAQ,kBAAkB;AAC5D,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA,WAKE,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA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","names":["path","options","path"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "offline-page-kit",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Framework-agnostic offline page + service worker generator (TypeScript)",
5
5
  "license": "MIT",
6
6
  "author": {