From 490ad9b7f1d5b5efcc4ed2be7fd5a7bd3fae8716 Mon Sep 17 00:00:00 2001 From: chris Date: Wed, 9 Sep 2026 18:18:19 -0400 Subject: [PATCH] PWA polish: PNG/maskable icons, install prompt, manifest shortcuts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rasterised the SVG to pwa-192 / pwa-512 / pwa-maskable-512 (logo in the safe zone, full-bleed background) and a real apple-touch-icon.png; manifest now lists PNG "any" + a dedicated "maskable". - Manifest shortcuts: Team, Search, Type chart. - lib/install.js captures beforeinstallprompt so the app can offer installation itself — a one-time toast, and an "Install app" button in Settings (falls back to a "use your browser menu" hint where the event never fires, e.g. iOS). - index.html: apple-touch-icon PNG + apple-mobile-web-app meta. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu --- README.md | 11 +++++---- index.html | 5 +++- public/apple-touch-icon.png | Bin 0 -> 4211 bytes public/pwa-192.png | Bin 0 -> 5942 bytes public/pwa-512.png | Bin 0 -> 16556 bytes public/pwa-maskable-512.png | Bin 0 -> 9132 bytes src/lib/install.js | 48 ++++++++++++++++++++++++++++++++++++ src/main.js | 11 +++++++++ src/styles/layout.css | 3 +++ src/views/SettingsView.js | 33 ++++++++++++++++++++++++- vite.config.js | 37 +++++++++++++++++++++++---- 11 files changed, 136 insertions(+), 12 deletions(-) create mode 100644 public/apple-touch-icon.png create mode 100644 public/pwa-192.png create mode 100644 public/pwa-512.png create mode 100644 public/pwa-maskable-512.png create mode 100644 src/lib/install.js diff --git a/README.md b/README.md index ad2e665..ed1c03a 100644 --- a/README.md +++ b/README.md @@ -127,12 +127,13 @@ Working app with a type-themed UI: feedback on catch (Vibration API), which screen to land on at launch, JSON export/import. - **PWA** — Workbox SW: precache shell + snapshot, SWR for API JSON, - cache-first LRU for sprites, in-app update toast. + cache-first LRU for sprites, in-app update toast. Installable: PNG + + maskable icons, `beforeinstallprompt` captured for an in-app "Install" + offer (toast + Settings button), manifest shortcuts to Team / Search / + Type chart. -Not yet done: version-exclusive badges, type/generation filters on the grid, -an interactive region map (PokéAPI has no map imagery or coordinates), -skeleton loaders, install-prompt handling, PNG/maskable raster icons -(currently SVG only), and a Lighthouse PWA pass. +Not yet done: version-exclusive badges, an interactive region map (PokéAPI +has no map imagery or coordinates), and a formal Lighthouse pass. ## Notes diff --git a/index.html b/index.html index 087ca3a..539d267 100644 --- a/index.html +++ b/index.html @@ -12,7 +12,10 @@ content="A responsive, offline-capable Pokédex reference powered by PokéAPI." /> - + + + + Pokédex diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..728b27e5d11f486e6c0cfd45de312c1b7192953d GIT binary patch literal 4211 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 V?tHQm+)V>|Rzl%r!3(2z{|5$$8(aVY literal 0 HcmV?d00001 diff --git a/public/pwa-maskable-512.png b/public/pwa-maskable-512.png new file mode 100644 index 0000000000000000000000000000000000000000..00e1b7e82741f11ed5dc079ebee77bd70d7e0f1a GIT binary patch literal 9132 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

eN^ox zwHxFB-Qe`{sIOkQXQ>xV=rU*}=fP4(7Z(#yV5Fs`UC?G?Vv326r`D&D$N8}hfxC)U zycjW4e9?=lz-B-nu|OzsI?111r+(Ia<*lE*$WRE|*ZtVlo+$!YBt#x*-gDgi63 zWCl-+$A7#Dn&y*AtFK#!=)Pvt(kulpx)a6!X+RfymxgmE#cNKpj~X7BU=}tPQ82pe zPC#a(gK#$Y@>ADmU7ZnM45-t9IW7RcaC`19{{cmvD1G1Z&tafRJE(g8(;7eF2Z7n= z{l1C9a=H(_Sk0Un)Iwo-H^c}<6nErZoIad72Ocs$pkr$r8(u^+bUK4m%zyLgdQA|P zfo^QYOo~mmKUD#OK}UeTc4)1Bw{M$RJ4ln&|9hZdWbB~i-!|W0A~#o&JQqxz3^1P! z(vX;*1{h$VpFV%aY^x5dd~X@eaNd#<$%a~&KLn+#;3Y7;h)^|6tsL8T1zw7R9pS)c zSLzSpS6rF5_CXn-1_llfKBoxul(8>O?GpkEw$X+n=Td>lH*KK{Y}& z$giv|bs9_n=ud%>O}YPIv=J6Bi6dI0iJVIJctJTtz7{> z03xC=Ec2iiCFN+y8g@ z0E~+Sr+@%;A!DUWJ|o4r5q~iE`rh|9zO#{ zRcHGQ4d3Zib^Q5abOjIr&r)CC4td6+*SDaPa?$nSx^Kb-?mC-cC>E4TQQQMyaU zR|({bJgA~}-1?tXWE4;bUI;L-a{w~#ypb=Qzx|3N>h__g`W~Oljz~pP%D)r0ixo-9 z4*L}OLL2K)upnn;T&+0MPe^_z)QmdrD=O%Wlp1NOqGQ31l)*{%qdEPPpktzWdk(s9 z?vF^88+I!fJE&`;oLo<6+Z-iEl|SzC*uZ(Warw%r`EPDrPSGv;Bdy|#3*BOCn2XqlIRH; zkf(F)=q=-+x(iDJ4ez1}O1T3`Cy8)&`XyP}!64!YHeeG5S~7z_Cn2|(7(`LU%rR@@ zr8xeQ{C80c-pgG$DgDWDwzmv4(A!(OW^d$%$->EXJ0NY$ki|y=`L;s94Dgai#YPWr=%ih~lwvG_&&e zR+PZpei6R$i@qVb&xOuy`M%g`LGtdGju{${HI&T09tWJ!)ijGDbK{NeZL_I!_w!JQ z;4_ukGj@ktQ97Ad^(G|4vN#wE_KKC3c3bglTULUMjF8~76XoCM>vsPlc7DxAsIq5R zmYeLJMc+f+@Byc3uz;7tp){#h@7K5jEDBAnr_nn1iY)g_HdHpM9jiP|ZwU^7Jt7h$ zuUpTKqVgk0e6s_}u9nP3JXO`okHhb4ol;)Ad^AWff)RSh{f(6`@719b=95cC@j%i; zqNh?}cBP4MX+xe6LBdGauLhw02HqA}o$SkxkKCK&mfb6|?iwE!uhCaE8tFhP?hG}H z`P)YCN&NddBt(cQy8|(!(r(m3kxss{U`g~uBPO%dlcoH1?OZS4?fD}F-kV(HfI^O5 zu=dL8F3l`leblQvI6qzqn<~I60@S1!@cr>_W|r;*9&sF~G5|E!=aqAtnM`D2mjoK8-FjeqGWF`6mDz`) z%Hq22W^Wdx-QMl6@a8CoGhhiJG%<1U@H-FQ2i)yiZEo3HdexGlr^oE&CDu7@3ZikZ zUBVGG4gRPXgGJb0D`as>f{`pYcTr7_vBb0;Sf_ZvNjC@B4ZJdZZ^;I}>Va~g|H0dI y7P)g75JV?I{a=9pzJvdtXzagQ`oFEEcZal{qinI-Cf}(c25D>RYm}(p5Bq;tB_m=0 literal 0 HcmV?d00001 diff --git a/src/lib/install.js b/src/lib/install.js new file mode 100644 index 0000000..ceff0a5 --- /dev/null +++ b/src/lib/install.js @@ -0,0 +1,48 @@ +/** + * Home-screen install: captures the browser's `beforeinstallprompt` so the + * app can offer installation on its own terms (a toast, a Settings button) + * instead of relying on the browser's mini-infobar. + * + * Import this early — the event fires once, soon after load. + */ +let deferred = null; +const listeners = new Set(); +const notify = () => { + for (const fn of listeners) fn(!!deferred); +}; + +if (typeof window !== 'undefined') { + window.addEventListener('beforeinstallprompt', (e) => { + e.preventDefault(); + deferred = e; + notify(); + }); + window.addEventListener('appinstalled', () => { + deferred = null; + notify(); + }); +} + +export function canInstall() { + return !!deferred; +} + +export function onInstallChange(fn) { + listeners.add(fn); + return () => listeners.delete(fn); +} + +/** Show the native install dialog. Resolves true if the user accepted. */ +export async function promptInstall() { + if (!deferred) return false; + deferred.prompt(); + let outcome = 'dismissed'; + try { + ({ outcome } = await deferred.userChoice); + } catch { + /* ignore */ + } + deferred = null; + notify(); + return outcome === 'accepted'; +} diff --git a/src/main.js b/src/main.js index 715752b..dde87fd 100644 --- a/src/main.js +++ b/src/main.js @@ -7,6 +7,7 @@ import { initRouter, rerender } from './router.js'; import { Nav } from './components/Nav.js'; import { settings, applyTheme } from './store/settings.js'; import { ui } from './store/ui.js'; +import { onInstallChange, promptInstall } from './lib/install.js'; applyTheme(); @@ -58,6 +59,16 @@ const updateSW = registerSW({ }, }); +// --- Install prompt: offer it once, unobtrusively --------------------- +let installOffered = false; +onInstallChange((can) => { + if (!can || installOffered) return; + installOffered = true; + showToast('Install this Pokédex for offline, full-screen use?', 'Install', () => { + promptInstall(); + }); +}); + // --- Offline indicator ------------------------------------------------- const netPill = el('div', { class: 'netpill', role: 'status' }, 'Offline'); function syncNet() { diff --git a/src/styles/layout.css b/src/styles/layout.css index 157d356..7056722 100644 --- a/src/styles/layout.css +++ b/src/styles/layout.css @@ -1591,6 +1591,9 @@ .settings__actions { flex-flow: row wrap; } +.settings__install { + margin: 10px 0 0; +} .field { display: flex; flex-direction: column; diff --git a/src/views/SettingsView.js b/src/views/SettingsView.js index 51a88e6..bcdc499 100644 --- a/src/views/SettingsView.js +++ b/src/views/SettingsView.js @@ -1,6 +1,7 @@ import { el, onTeardown } from '../lib/dom.js'; import { settings, applyTheme } from '../store/settings.js'; import { selection } from '../store/selection.js'; +import { canInstall, onInstallChange, promptInstall } from '../lib/install.js'; export async function SettingsView() { const view = el('section', { class: 'view settings' }); @@ -136,8 +137,36 @@ export async function SettingsView() { }, }); + // Install (only when the browser has offered it and the app isn't + // already installed). + const installRow = el('div', { class: 'settings__install' }); + function syncInstall() { + installRow.replaceChildren( + canInstall() + ? el( + 'button', + { + class: 'button', + type: 'button', + onclick: () => promptInstall(), + }, + 'Install app', + ) + : el( + 'p', + { class: 'settings__note' }, + window.matchMedia('(display-mode: standalone)').matches + ? 'Running as an installed app.' + : 'Use your browser menu to add this Pokédex to your home screen.', + ), + ); + } + syncInstall(); + const offInstall = onInstallChange(syncInstall); + view.append( el('header', { class: 'view__header' }, el('h1', {}, 'Settings')), + installRow, el( 'div', { class: 'settings__group' }, @@ -196,7 +225,9 @@ export async function SettingsView() { ), ); - onTeardown(view, () => {}); + onTeardown(view, () => { + offInstall(); + }); return view; } diff --git a/vite.config.js b/vite.config.js index 63fff09..7b3d5a9 100644 --- a/vite.config.js +++ b/vite.config.js @@ -21,7 +21,11 @@ export default defineConfig({ // The bundled PokéAPI snapshot can be a few hundred KB. maximumFileSizeToCacheInBytes: 5 * 1024 * 1024, }, - includeAssets: ['icon.svg', 'favicon.svg'], + includeAssets: [ + 'icon.svg', + 'favicon.svg', + 'apple-touch-icon.png', + ], manifest: { name: 'Pokédex', short_name: 'Pokédex', @@ -35,11 +39,34 @@ export default defineConfig({ theme_color: '#b3161a', categories: ['games', 'reference', 'utilities'], icons: [ + { src: 'icon.svg', sizes: 'any', type: 'image/svg+xml', purpose: 'any' }, + { src: 'pwa-192.png', sizes: '192x192', type: 'image/png', purpose: 'any' }, + { src: 'pwa-512.png', sizes: '512x512', type: 'image/png', purpose: 'any' }, { - src: 'icon.svg', - sizes: 'any', - type: 'image/svg+xml', - purpose: 'any maskable', + src: 'pwa-maskable-512.png', + sizes: '512x512', + type: 'image/png', + purpose: 'maskable', + }, + ], + shortcuts: [ + { + name: 'Team builder', + short_name: 'Team', + url: './#/team', + icons: [{ src: 'pwa-192.png', sizes: '192x192' }], + }, + { + name: 'Search', + short_name: 'Search', + url: './#/search', + icons: [{ src: 'pwa-192.png', sizes: '192x192' }], + }, + { + name: 'Type chart', + short_name: 'Types', + url: './#/types', + icons: [{ src: 'pwa-192.png', sizes: '192x192' }], }, ], },