feat(pwa): installable app + fix research tree pip overflow
Research tree UI: - The level pips shared the header row with the upgrade name and fought for width; long names like "Festungsmauern" (5 pips) pushed the pips out of the upgrade card. Pips now sit in the bottom row next to the buy button (space-between), where width is plentiful, and carry a "level / max" tooltip - Tighter modal padding on phones (max-width 560px), scrollable at 92vh PWA (installable as app in mobile browsers): - public/manifest.webmanifest: standalone display, any orientation, TRXTD theme colors, German lang, start_url "/" - public/sw.js: network-first service worker with runtime caching. index.html is always fetched fresh so new deploys are picked up immediately; the cache only serves as offline fallback. API calls and cross-origin requests are never intercepted - Icon set generated by scripts/generate-icons.mjs (pure Node PNG encoder, zero image dependencies): gold tower on the game-themed dark gradient in 192px, 512px, maskable 512px (motif inside the safe zone) and 180px apple-touch-icon - index.html: manifest link, favicon, apple-touch-icon, theme-color, mobile-web-app-capable, apple-mobile-web-app meta tags - src/main.ts: service worker registration in production builds only (dev HMR stays untouched) - server.mjs: serve .webmanifest as application/manifest+json (Chrome requires the correct MIME type for installability) Verified against the production server: manifest (application/ manifest+json), icons (image/png) and sw.js (text/javascript) respond with 200, index.html references all PWA tags; npm test 48/48 green.
This commit is contained in:
parent
914af42210
commit
31cb594cb0
12 changed files with 296 additions and 13 deletions
|
|
@ -58,19 +58,21 @@ function close(): void {
|
|||
<div class="u-top">
|
||||
<span class="u-icon">{{ def.icon }}</span>
|
||||
<span class="u-name">{{ def.name }}</span>
|
||||
<span class="pips">
|
||||
<i v-for="n in def.maxLevel" :key="n" :class="{ on: levelOf(def.id) >= n }" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="u-desc">{{ def.desc }}</div>
|
||||
<div class="u-effect">{{ def.effectDesc(Math.max(1, levelOf(def.id))) }}</div>
|
||||
<button
|
||||
class="buy"
|
||||
:disabled="nextCost(def) === null || crystals < (nextCost(def) ?? 0)"
|
||||
@click="buy(def)"
|
||||
>
|
||||
{{ nextCost(def) === null ? 'MAX' : `💎 ${nextCost(def)}` }}
|
||||
</button>
|
||||
<div class="u-bottom">
|
||||
<button
|
||||
class="buy"
|
||||
:disabled="nextCost(def) === null || crystals < (nextCost(def) ?? 0)"
|
||||
@click="buy(def)"
|
||||
>
|
||||
{{ nextCost(def) === null ? 'MAX' : `💎 ${nextCost(def)}` }}
|
||||
</button>
|
||||
<span class="pips" :title="`${levelOf(def.id)} / ${def.maxLevel}`">
|
||||
<i v-for="n in def.maxLevel" :key="n" :class="{ on: levelOf(def.id) >= n }" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -178,6 +180,7 @@ h2 {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
.u-icon {
|
||||
font-size: 18px;
|
||||
|
|
@ -185,11 +188,18 @@ h2 {
|
|||
.u-name {
|
||||
font-weight: 700;
|
||||
font-size: 13.5px;
|
||||
flex: 1;
|
||||
}
|
||||
.u-bottom {
|
||||
margin-top: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
.pips {
|
||||
display: flex;
|
||||
gap: 3px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.pips i {
|
||||
width: 8px;
|
||||
|
|
@ -211,7 +221,6 @@ h2 {
|
|||
font-weight: 700;
|
||||
}
|
||||
.buy {
|
||||
margin-top: 4px;
|
||||
background: var(--panel-inset);
|
||||
border: 1px solid #3d6f9e;
|
||||
color: var(--text);
|
||||
|
|
@ -221,7 +230,6 @@ h2 {
|
|||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
align-self: flex-start;
|
||||
}
|
||||
.buy:hover:not(:disabled) {
|
||||
border-color: var(--accent);
|
||||
|
|
@ -235,4 +243,12 @@ h2 {
|
|||
color: #ff8a7a;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Phones: tighter modal padding, one upgrade column */
|
||||
@media (max-width: 560px) {
|
||||
.card {
|
||||
padding: 16px 14px;
|
||||
max-height: 92vh;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
10
src/main.ts
10
src/main.ts
|
|
@ -5,3 +5,13 @@ import { initAuth } from './game/auth'
|
|||
|
||||
createApp(App).mount('#app')
|
||||
void initAuth()
|
||||
|
||||
// PWA: register the service worker (installable app + offline play).
|
||||
// Dev mode is excluded so vite's HMR is never bypassed by a cached shell.
|
||||
if (import.meta.env.PROD && 'serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/sw.js').catch(() => {
|
||||
/* offline support is optional – ignore registration errors */
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue