web: the muted text that was never muted, and the stylesheet fetched every time

Every text-[color:var(--ink)]/50, border-[color:var(--ink)]/10 and
bg-[color:var(--ink)]/5 in the templates compiled to nothing. Tailwind v3
cannot put an opacity on an arbitrary var() colour, so all 538 of them were
dropped from output.css without a word. Meta text rendered at full ink, card
borders fell back to Tailwind's default grey (bright white rings at night),
and the washes, backdrops and hover pills simply weren't there, on the news
site, the casino and the adventure pages alike.

The palette is now written once per phase and casino room as bare rgb
channels (--ink-rgb and friends), the plain vars are built from them, and
tailwind.config.js exposes ink, muted, accent, warn and card as real colours.
The classes are text-ink/80, border-ink/10, bg-accent/20 now, and they compile.

Restoring the intended opacities as-is would have traded flat for illegible:
ink at 50% measures 2.7 to 2.9:1 on the lit phases. So secondary text gets
its own colour, --muted, per phase and per casino room, taken as faint as it
can go while still clearing 4.5:1 on both the card and the page. Everything
that was ink at 35 to 70% is text-muted. The 20-30% ones are separators and
empty-cell dashes and stay faint.

The bg-theme-* chips carry white text everywhere they appear, and eight of
them measured 2.5 to 3.2:1. Same hues, taken just dark enough to pass (4.6 to
4.7:1). eu, lego and adventure already passed and are unchanged, and the
underline and border colours keep the old pastels since they carry no text.

Static files came out of the embedded FS with no Last-Modified, no ETag and
no Cache-Control, so every page view downloaded the stylesheet and every
script in full, and the service worker's "cheap 304" never happened. Each
file is hashed once now. Templates link through {{asset}}, which appends
?v=<hash>; a request naming the current hash is served immutable for a year,
and everything else gets the hash as its ETag and revalidates to a 304. The
service worker serves versioned URLs cache-first, drops older versions of the
same file as new ones land, and moves to cache v6.
This commit is contained in:
prosolis
2026-09-22 22:54:50 -07:00
parent 4eaf8ae366
commit a15d67e210
40 changed files with 900 additions and 676 deletions
+67 -31
View File
@@ -17,41 +17,68 @@
amber-400 on cream at 1.55:1 — half of the four combinations unreadable, and
which half depends on a setting outside the page. Only night has a dark card;
dusk and dawn are lit despite their names. */
/* Each colour is written once, as bare rgb channels (--ink-rgb), and the plain
var (--ink) is built from it. Tailwind needs the channels: tailwind.config.js
maps ink/muted/accent/warn/card to rgb(var(--x-rgb) / <alpha-value>), which is
what makes text-ink/80 or border-ink/10 actually compile. The old spelling,
text-[color:var(--ink)]/80, never did: Tailwind v3 cannot put an opacity on
an arbitrary var() colour, so all 538 of those classes silently produced no
rule at all.
--muted is the secondary-text colour: ink mixed into the card, as faint as it
can go while still clearing 4.5:1 on both the card and the page background.
It exists because ink-at-50% looks right but measures 2.7–2.9:1 on the lit
phases. Use text-muted for meta text, not text-ink/50. */
:root,
html[data-phase="day"] {
--bg: #fff7e4; /* warm cream */
--bg-grad: #ffeec2; /* soft sun wash */
--card: #ffffff;
--ink: #3a2e1f;
--accent: #f2a541; /* sunshine yellow */
--warn: #b45309;
--card-rgb: 255 255 255;
--ink-rgb: 58 46 31;
--muted-rgb: 119 111 100; /* 4.95:1 card, 4.64:1 bg */
--accent-rgb: 242 165 65; /* sunshine yellow */
--warn-rgb: 180 83 9;
}
html[data-phase="dawn"] {
--bg: #ffe7d6;
--bg-grad: #ffc9c9;
--card: #fff4ea;
--ink: #4a2e2a;
--accent: #ff8a65;
--warn: #b45309;
--card-rgb: 255 244 234;
--ink-rgb: 74 46 42;
--muted-rgb: 123 99 94; /* 5.12:1 card, 4.66:1 bg */
--accent-rgb: 255 138 101;
--warn-rgb: 180 83 9;
}
html[data-phase="dusk"] {
--bg: #ffd6a8;
--bg-grad: #f7a07e;
--card: #fff1de;
--ink: #3d2417;
--accent: #e6553a;
--warn: #b45309;
--card-rgb: 255 241 222;
--ink-rgb: 61 36 23;
--muted-rgb: 113 91 77; /* 5.71:1 card, 4.67:1 bg */
--accent-rgb: 230 85 58;
--warn-rgb: 180 83 9;
}
html[data-phase="night"] {
--bg: #1a1f3a;
--bg-grad: #2a3358;
--card: #2d365a;
--ink: #f1ecd8; /* moonlight */
--accent: #f9d976; /* lantern */
--warn: #fbbf24; /* the only dark card, so the only light amber */
--card-rgb: 45 54 90;
--ink-rgb: 241 236 216; /* moonlight */
--muted-rgb: 163 163 166; /* 4.67:1 card, 6.42:1 bg */
--accent-rgb: 249 217 118; /* lantern */
--warn-rgb: 251 191 36; /* the only dark card, so the only light amber */
}
/* The plain vars, built from the channels. Declared on html alongside every
palette block (phase and casino room alike), so a block that only swaps the
channels still gets matching plain vars. */
html {
--card: rgb(var(--card-rgb));
--ink: rgb(var(--ink-rgb));
--muted: rgb(var(--muted-rgb));
--accent: rgb(var(--accent-rgb));
--warn: rgb(var(--warn-rgb));
}
@layer base {
@@ -81,16 +108,23 @@ html[data-phase="night"] {
.shadow-pete-lg { box-shadow: 0 6px 0 rgba(60,40,20,0.12), 0 16px 32px rgba(60,40,20,0.12); }
/* Per-channel theme colors. Kept as plain utilities so Tailwind doesn't
need to know to safelist them. */
.bg-theme-gaming { background-color: #4caf7d; }
.bg-theme-tech { background-color: #5aa9e6; }
.bg-theme-politics { background-color: #e07a5f; }
need to know to safelist them.
Every bg-theme-* carries white text (chips, the active nav pill, channel
heroes), so each one has to clear 4.5:1 against white. The original pastels
measured 2.5–3.2:1; these are the same hues taken just dark enough to pass
(4.6–4.7:1). eu, lego and adventure already passed and are untouched. The
decoration-theme-* underlines below keep the original pastels: they carry
no text. */
.bg-theme-gaming { background-color: #38815c; } /* was #4caf7d */
.bg-theme-tech { background-color: #1d79c0; } /* was #5aa9e6 */
.bg-theme-politics { background-color: #cb4a27; } /* was #e07a5f */
.bg-theme-eu { background-color: #003399; }
.bg-theme-music { background-color: #b079d6; }
.bg-theme-anime { background-color: #ec5e8a; }
.bg-theme-foss { background-color: #d97706; }
.bg-theme-kids { background-color: #14b8a6; }
.bg-theme-finance { background-color: #10b981; }
.bg-theme-music { background-color: #9a54cb; } /* was #b079d6 */
.bg-theme-anime { background-color: #e01a58; } /* was #ec5e8a */
.bg-theme-foss { background-color: #af6005; } /* was #d97706 */
.bg-theme-kids { background-color: #0e8376; } /* was #14b8a6 */
.bg-theme-finance { background-color: #0c855d; } /* was #10b981 */
.bg-theme-lego { background-color: #d01012; }
.bg-theme-adventure { background-color: #6d4bd8; }
@@ -2285,9 +2319,10 @@ html[data-phase="night"] {
html[data-room="casinopolis"] {
--bg: #16211c; /* the room, lights low */
--card: #23342b; /* a table in it */
--ink: #f6ecd8; /* warm cream, not white */
--accent: #f2b53d; /* honey */
--card-rgb: 35 52 43; /* a table in it */
--ink-rgb: 246 236 216; /* warm cream, not white */
--muted-rgb: 153 155 140; /* 4.65:1 card, 5.85:1 bg */
--accent-rgb: 242 181 61; /* honey */
--felt-a: #2f7d5b;
--felt-b: #24614a;
--felt-c: #1c4d3c;
@@ -2296,9 +2331,10 @@ html[data-room="casinopolis"] {
html[data-room="casino-night"] {
--bg: #140f2e; /* the machine's cabinet */
--card: #241a4d;
--ink: #f2ecff;
--accent: #ffcc2f; /* the jackpot bulb */
--card-rgb: 36 26 77;
--ink-rgb: 242 236 255;
--muted-rgb: 143 135 170; /* 4.67:1 card, 5.46:1 bg */
--accent-rgb: 255 204 47; /* the jackpot bulb */
--felt-a: #4a2fa8; /* the table is a pinball table now */
--felt-b: #351f80;
--felt-c: #241659;
File diff suppressed because one or more lines are too long
+8 -8
View File
@@ -156,12 +156,12 @@
// and pushed the verb itself onto two lines.
var li = document.createElement('li');
var verb = document.createElement('div');
verb.className = 'font-semibold text-[color:var(--ink)]/70';
verb.className = 'font-semibold text-muted';
verb.textContent = VERB[o.action] || o.action;
var said = document.createElement('div');
said.className = 'mt-0.5 leading-snug ' + (o.status === 'pending'
? 'text-[color:var(--ink)]/45'
: (o.status === 'applied' ? 'text-theme-adventure font-semibold' : 'text-[color:var(--warn)]'));
? 'text-muted'
: (o.status === 'applied' ? 'text-theme-adventure font-semibold' : 'text-warn'));
said.textContent = o.detail || STATUS[o.status] || o.status;
li.appendChild(verb); li.appendChild(said);
list.appendChild(li);
@@ -247,14 +247,14 @@
if (existing) existing.remove();
var boxEl = document.createElement('div');
boxEl.className = 'adv-action-confirm mt-3 rounded-xl bg-[color:var(--ink)]/5 p-3 text-sm';
boxEl.className = 'adv-action-confirm mt-3 rounded-xl bg-ink/5 p-3 text-sm';
var p = document.createElement('p');
p.className = 'text-[color:var(--ink)]/70';
p.className = 'text-muted';
p.textContent = btn.getAttribute('data-confirm') || 'Are you sure?';
var cost = parseFloat(btn.getAttribute('data-cost') || '0');
if (cost > 0) {
var money = document.createElement('p');
money.className = 'mt-1.5 font-semibold text-[color:var(--ink)]/80';
money.className = 'mt-1.5 font-semibold text-ink/80';
if (panelBalance - cost < 0) {
// No arrow when it does not cover. The game can carry a small debt, so
// the resulting figure is not always nonsense — but printing "€-6,300"
@@ -262,7 +262,7 @@
// lands on the wrong side of the sign.
money.textContent = '€' + euroFmt(cost) + " — you have €" + euroFmt(panelBalance) +
". That won't cover it.";
money.className += ' text-[color:var(--warn)]';
money.className += ' text-warn';
} else {
money.textContent = '€' + euroFmt(cost) + ' — balance €' + euroFmt(panelBalance) +
' → €' + euroFmt(panelBalance - cost) + '.';
@@ -293,7 +293,7 @@
yes.addEventListener('click', function () { boxEl.remove(); placeOrder(btn); });
var no = document.createElement('button');
no.type = 'button';
no.className = 'rounded-full border border-[color:var(--ink)]/20 text-[color:var(--ink)]/60 px-3 py-1';
no.className = 'rounded-full border border-ink/20 text-muted px-3 py-1';
no.textContent = 'Not yet';
no.addEventListener('click', function () { boxEl.remove(); });
row.appendChild(yes); row.appendChild(no);
+2 -2
View File
@@ -844,12 +844,12 @@
var stakes = tierBtns.filter(function (b) { return b.dataset.tier === t.tier; })[0];
var btn = document.createElement("button");
btn.type = "button";
btn.className = "flex items-center justify-between gap-3 rounded-2xl border-2 border-[color:var(--ink)]/10 p-3 text-left hover:bg-[color:var(--ink)]/5 transition";
btn.className = "flex items-center justify-between gap-3 rounded-2xl border-2 border-ink/10 p-3 text-left hover:bg-ink/5 transition";
var label = document.createElement("span");
label.className = "font-display font-bold";
label.textContent = stakes ? stakes.querySelector(".font-display").textContent : t.tier;
var seats = document.createElement("span");
seats.className = "text-xs font-semibold text-[color:var(--ink)]/50 tabular-nums";
seats.className = "text-xs font-semibold text-muted tabular-nums";
seats.textContent = t.humans + "/" + t.seats + " seated";
btn.appendChild(label);
btn.appendChild(seats);
+8 -8
View File
@@ -58,29 +58,29 @@
}
const html = items.map((r, i) => {
const thumb = r.thumb_url
? `<div class="hidden sm:block w-20 h-20 shrink-0 overflow-hidden rounded-2xl bg-[color:var(--ink)]/5">
? `<div class="hidden sm:block w-20 h-20 shrink-0 overflow-hidden rounded-2xl bg-ink/5">
<img src="${escapeHTML(r.thumb_url)}" alt="${escapeHTML(r.headline || "")}" loading="lazy" decoding="async"
class="h-full w-full object-cover">
</div>`
: `<div class="hidden sm:flex w-20 h-20 shrink-0 items-center justify-center rounded-2xl bg-[color:var(--ink)]/5 text-2xl">${escapeHTML(r.channel_emoji || "·")}</div>`;
: `<div class="hidden sm:flex w-20 h-20 shrink-0 items-center justify-center rounded-2xl bg-ink/5 text-2xl">${escapeHTML(r.channel_emoji || "·")}</div>`;
const postedRing = r.posted ? ` border-theme-${escapeHTML(r.channel_theme)}` : "";
const href = safeURL(r.article_url);
if (!href) return "";
return `
<a href="${escapeHTML(href)}" target="_blank" rel="noopener noreferrer"
data-idx="${i}"
class="search-result group flex items-start gap-4 rounded-2xl p-3 transition border-2 border-transparent hover:bg-[color:var(--ink)]/5${postedRing}">
class="search-result group flex items-start gap-4 rounded-2xl p-3 transition border-2 border-transparent hover:bg-ink/5${postedRing}">
${thumb}
<div class="min-w-0 flex-1 space-y-1">
<div class="flex items-center gap-2 text-xs flex-wrap">
<span class="inline-flex items-center rounded-full bg-theme-${escapeHTML(r.channel_theme)} text-white px-2.5 py-0.5 font-semibold uppercase tracking-wider">
<span aria-hidden="true" class="mr-1">${escapeHTML(r.channel_emoji)}</span>${escapeHTML(r.channel_title)}
</span>
<span class="text-[color:var(--ink)]/60 font-semibold">${escapeHTML(r.source)}</span>
<span class="text-[color:var(--ink)]/50">${escapeHTML(r.time_ago)}</span>
<span class="text-muted font-semibold">${escapeHTML(r.source)}</span>
<span class="text-muted">${escapeHTML(r.time_ago)}</span>
</div>
<h3 class="font-display text-base sm:text-lg font-semibold leading-snug">${escapeHTML(r.headline)}</h3>
${r.lede ? `<p class="text-sm text-[color:var(--ink)]/70 line-clamp-2">${escapeHTML(r.lede)}</p>` : ""}
${r.lede ? `<p class="text-sm text-muted line-clamp-2">${escapeHTML(r.lede)}</p>` : ""}
</div>
</a>`;
}).join("");
@@ -92,10 +92,10 @@
const nodes = list.querySelectorAll(".search-result");
nodes.forEach((n, i) => {
if (i === activeIndex) {
n.classList.add("bg-[color:var(--ink)]/5", "border-[color:var(--ink)]/15");
n.classList.add("bg-ink/5", "border-ink/15");
n.scrollIntoView({ block: "nearest" });
} else {
n.classList.remove("bg-[color:var(--ink)]/5", "border-[color:var(--ink)]/15");
n.classList.remove("bg-ink/5", "border-ink/15");
}
});
}
+4 -4
View File
@@ -67,28 +67,28 @@
var sources = knownSources();
list.innerHTML = "";
if (sources.length === 0) {
list.innerHTML = '<p class="px-2 py-4 text-sm text-center text-[color:var(--ink)]/50">No feeds configured.</p>';
list.innerHTML = '<p class="px-2 py-4 text-sm text-center text-muted">No feeds configured.</p>';
} else {
var lastChannel = null;
sources.forEach(function (src, i) {
if (src.channel !== lastChannel) {
lastChannel = src.channel;
var hdr = document.createElement("div");
hdr.className = "px-3 pt-3 pb-1 text-[11px] uppercase tracking-wider text-[color:var(--ink)]/40 font-bold";
hdr.className = "px-3 pt-3 pb-1 text-[11px] uppercase tracking-wider text-muted font-bold";
hdr.textContent = src.channel || "other";
list.appendChild(hdr);
}
var id = "pete-feed-" + i;
var row = document.createElement("label");
row.setAttribute("for", id);
row.className = "flex items-center justify-between gap-3 rounded-2xl px-3 py-2 hover:bg-[color:var(--ink)]/5 cursor-pointer";
row.className = "flex items-center justify-between gap-3 rounded-2xl px-3 py-2 hover:bg-ink/5 cursor-pointer";
var nameEl = document.createElement("span");
nameEl.className = "text-sm font-semibold truncate";
nameEl.textContent = src.name;
var box = document.createElement("input");
box.type = "checkbox";
box.id = id;
box.className = "h-4 w-4 shrink-0 accent-[color:var(--accent)]";
box.className = "h-4 w-4 shrink-0 accent-accent";
box.checked = !disabled[src.name];
box.addEventListener("change", function () {
if (box.checked) delete disabled[src.name];
+2 -2
View File
@@ -1018,12 +1018,12 @@
var stakes = root.querySelector('[data-tier="' + t.tier + '"]');
var btn = document.createElement("button");
btn.type = "button";
btn.className = "flex items-center justify-between gap-3 rounded-2xl border-2 border-[color:var(--ink)]/10 p-3 text-left hover:bg-[color:var(--ink)]/5 transition";
btn.className = "flex items-center justify-between gap-3 rounded-2xl border-2 border-ink/10 p-3 text-left hover:bg-ink/5 transition";
var lbl = document.createElement("span");
lbl.className = "font-display font-bold";
lbl.textContent = stakes ? stakes.querySelector(".font-display").textContent : t.tier;
var seats = document.createElement("span");
seats.className = "text-xs font-semibold text-[color:var(--ink)]/50 tabular-nums";
seats.className = "text-xs font-semibold text-muted tabular-nums";
seats.textContent = t.humans + "/" + t.seats + " seated";
btn.appendChild(lbl);
btn.appendChild(seats);
+23 -23
View File
@@ -207,10 +207,10 @@
if (!cardMount) return;
var air = describeAqi(entry.aqi);
var aqiRow = air
? '<div class="flex items-center gap-2 border-t border-[color:var(--ink)]/10 px-5 py-3 text-sm">' +
? '<div class="flex items-center gap-2 border-t border-ink/10 px-5 py-3 text-sm">' +
'<span class="inline-block w-2.5 h-2.5 rounded-full shrink-0" style="background:' + air.color + '"></span>' +
'<span class="font-bold tabular-nums">AQI ' + entry.aqi + '</span>' +
'<span class="text-[color:var(--ink)]/60">' + air.label + '</span>' +
'<span class="text-muted">' + air.label + '</span>' +
'</div>'
: '';
var daily = entry.daily;
@@ -219,28 +219,28 @@
var dd = describe(daily.weather_code[i], 1);
days +=
'<div class="flex flex-col items-center gap-1 rounded-2xl px-2 py-2 min-w-0">' +
'<span class="text-xs font-semibold text-[color:var(--ink)]/60">' + (i === 0 ? "Today" : weekday(daily.time[i])) + '</span>' +
'<span class="text-xs font-semibold text-muted">' + (i === 0 ? "Today" : weekday(daily.time[i])) + '</span>' +
'<span class="text-2xl leading-none" aria-hidden="true">' + dd.emoji + '</span>' +
'<span class="text-sm font-bold tabular-nums">' + Math.round(daily.temperature_2m_max[i]) + '°</span>' +
'<span class="text-xs tabular-nums text-[color:var(--ink)]/50">' + Math.round(daily.temperature_2m_min[i]) + '°</span>' +
'<span class="text-xs tabular-nums text-muted">' + Math.round(daily.temperature_2m_min[i]) + '°</span>' +
'</div>';
}
cardMount.innerHTML =
'<div class="rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 shadow-pete overflow-hidden">' +
'<div class="rounded-3xl bg-card border-2 border-ink/10 shadow-pete overflow-hidden">' +
'<div class="flex flex-wrap items-center justify-between gap-4 p-5">' +
'<div class="flex items-center gap-4 min-w-0">' +
'<span class="text-5xl leading-none" aria-hidden="true">' + desc.emoji + '</span>' +
'<div class="min-w-0">' +
'<div class="flex items-baseline gap-2">' +
'<span class="font-display text-4xl font-bold tabular-nums">' + temp(cur.temperature_2m, sym) + '</span>' +
'<span class="text-sm font-semibold text-[color:var(--ink)]/70 truncate">' + desc.label + '</span>' +
'<span class="text-sm font-semibold text-muted truncate">' + desc.label + '</span>' +
'</div>' +
'<div class="text-sm text-[color:var(--ink)]/60 truncate">📍 ' + escapeHtml(loc.name) + '</div>' +
'<div class="text-sm text-muted truncate">📍 ' + escapeHtml(loc.name) + '</div>' +
'</div>' +
'</div>' +
'<button type="button" data-weather-loc class="shrink-0 rounded-full px-3 py-1.5 text-xs font-semibold text-[color:var(--ink)]/60 hover:bg-[color:var(--ink)]/5 border-2 border-[color:var(--ink)]/10 transition">Change location</button>' +
'<button type="button" data-weather-loc class="shrink-0 rounded-full px-3 py-1.5 text-xs font-semibold text-muted hover:bg-ink/5 border-2 border-ink/10 transition">Change location</button>' +
'</div>' +
'<div class="grid grid-cols-5 gap-1 border-t border-[color:var(--ink)]/10 bg-[color:var(--ink)]/[0.03] px-2 py-3">' + days + '</div>' +
'<div class="grid grid-cols-5 gap-1 border-t border-ink/10 bg-ink/[0.03] px-2 py-3">' + days + '</div>' +
aqiRow +
'</div>';
// The injected "Change location" button needs the same handler.
@@ -259,7 +259,7 @@
setAqiChip(null);
if (cardMount && getLoc()) {
cardMount.innerHTML =
'<div class="rounded-3xl bg-[color:var(--card)] border-2 border-dashed border-[color:var(--ink)]/15 p-5 text-center text-sm text-[color:var(--ink)]/60">' +
'<div class="rounded-3xl bg-card border-2 border-dashed border-ink/15 p-5 text-center text-sm text-muted">' +
escapeHtml(msg) + ' · <button type="button" data-weather-loc class="font-semibold underline">try again</button>' +
'</div>';
var b = cardMount.querySelector("[data-weather-loc]");
@@ -278,9 +278,9 @@
if (!popover) return;
popover.querySelectorAll("[data-loc-unit]").forEach(function (b) {
var on = b.getAttribute("data-loc-unit") === selectedUnit;
b.classList.toggle("bg-[color:var(--accent)]", on);
b.classList.toggle("bg-accent", on);
b.classList.toggle("text-white", on);
b.classList.toggle("text-[color:var(--ink)]/60", !on);
b.classList.toggle("text-muted", !on);
});
}
@@ -293,32 +293,32 @@
function buildPopover() {
var dlg = document.createElement("div");
dlg.id = "pete-weather-loc";
dlg.className = "hidden fixed inset-0 z-50 bg-[color:var(--ink)]/40 backdrop-blur-sm flex items-start justify-center pt-[12vh] px-4";
dlg.className = "hidden fixed inset-0 z-50 bg-ink/40 backdrop-blur-sm flex items-start justify-center pt-[12vh] px-4";
dlg.setAttribute("role", "dialog");
dlg.setAttribute("aria-modal", "true");
dlg.innerHTML =
'<div class="w-full max-w-sm rounded-3xl bg-[color:var(--card)] shadow-pete-lg border-2 border-[color:var(--ink)]/10 overflow-hidden">' +
'<div class="flex items-center justify-between gap-3 px-5 py-4 border-b border-[color:var(--ink)]/10">' +
'<div class="w-full max-w-sm rounded-3xl bg-card shadow-pete-lg border-2 border-ink/10 overflow-hidden">' +
'<div class="flex items-center justify-between gap-3 px-5 py-4 border-b border-ink/10">' +
'<h2 class="font-display text-lg font-bold">Weather location</h2>' +
'<button type="button" data-loc-close class="rounded-full px-2 py-1 text-sm text-[color:var(--ink)]/60 hover:bg-[color:var(--ink)]/5">esc</button>' +
'<button type="button" data-loc-close class="rounded-full px-2 py-1 text-sm text-muted hover:bg-ink/5">esc</button>' +
'</div>' +
'<div class="p-5 space-y-3">' +
'<p class="text-xs text-[color:var(--ink)]/60">Enter your postal code to see the local forecast. The background follows the weather. Saved in this browser only.</p>' +
'<p class="text-xs text-muted">Enter your postal code to see the local forecast. The background follows the weather. Saved in this browser only.</p>' +
'<div class="flex gap-2">' +
'<select data-loc-country class="rounded-2xl border-2 border-[color:var(--ink)]/10 bg-[color:var(--bg)] px-3 py-2 text-sm font-semibold outline-none focus:border-[color:var(--accent)]"></select>' +
'<input data-loc-postal type="text" inputmode="text" autocomplete="postal-code" placeholder="Postal code" class="flex-1 min-w-0 rounded-2xl border-2 border-[color:var(--ink)]/10 bg-[color:var(--bg)] px-3 py-2 text-sm outline-none focus:border-[color:var(--accent)]">' +
'<select data-loc-country class="rounded-2xl border-2 border-ink/10 bg-[color:var(--bg)] px-3 py-2 text-sm font-semibold outline-none focus:border-accent"></select>' +
'<input data-loc-postal type="text" inputmode="text" autocomplete="postal-code" placeholder="Postal code" class="flex-1 min-w-0 rounded-2xl border-2 border-ink/10 bg-[color:var(--bg)] px-3 py-2 text-sm outline-none focus:border-accent">' +
'</div>' +
'<div class="flex items-center justify-between gap-2">' +
'<span class="text-xs font-semibold text-[color:var(--ink)]/60">Units</span>' +
'<div class="inline-flex rounded-full border-2 border-[color:var(--ink)]/10 p-0.5 text-xs font-bold">' +
'<span class="text-xs font-semibold text-muted">Units</span>' +
'<div class="inline-flex rounded-full border-2 border-ink/10 p-0.5 text-xs font-bold">' +
'<button type="button" data-loc-unit="celsius" class="rounded-full px-3 py-1 transition">°C</button>' +
'<button type="button" data-loc-unit="fahrenheit" class="rounded-full px-3 py-1 transition">°F</button>' +
'</div>' +
'</div>' +
'<p data-loc-error class="hidden text-xs font-semibold text-red-500"></p>' +
'<div class="flex items-center justify-between gap-2 pt-1">' +
'<button type="button" data-loc-clear class="rounded-full px-3 py-2 text-xs font-semibold text-[color:var(--ink)]/50 hover:text-[color:var(--ink)]">Clear</button>' +
'<button type="button" data-loc-save class="rounded-full bg-[color:var(--accent)] px-5 py-2 text-sm font-bold text-white shadow-pete hover:brightness-105 transition disabled:opacity-50">Save</button>' +
'<button type="button" data-loc-clear class="rounded-full px-3 py-2 text-xs font-semibold text-muted hover:text-ink">Clear</button>' +
'<button type="button" data-loc-save class="rounded-full bg-accent px-5 py-2 text-sm font-bold text-white shadow-pete hover:brightness-105 transition disabled:opacity-50">Save</button>' +
'</div>' +
'</div>' +
'</div>';
+34 -16
View File
@@ -4,12 +4,13 @@
//
// Bump CACHE_VERSION whenever the precached shell assets change; activate()
// drops every cache that doesn't match the current version.
var CACHE_VERSION = "v5";
var CACHE_VERSION = "v6";
var SHELL_CACHE = "pete-shell-" + CACHE_VERSION;
var RUNTIME_CACHE = "pete-runtime-" + CACHE_VERSION;
// App shell: the static assets every page needs. Versioned by URL content only
// loosely, so we lean on network-first for HTML and cache-first for these.
// App shell: the static assets every page needs, precached by bare URL so an
// offline launch has something to fall back on. Pages ask for them with a
// ?v=<hash> suffix; see the fetch handler for how the two meet.
var SHELL_ASSETS = [
"/static/css/output.css",
"/static/js/prefs.js",
@@ -117,25 +118,42 @@ self.addEventListener("fetch", function (event) {
return;
}
// Static assets: network-first. Our asset URLs are NOT content-hashed
// (/static/js/weather-gl.js stays the same URL across deploys), so a
// cache-first strategy would pin whatever bytes were first cached and never
// notice a redeployed file changed — leaving stale JS/CSS in place until the
// CACHE_VERSION bump below. Go's FileServer sets ETag/Last-Modified, so an
// online refetch is a cheap 304 when nothing changed. We still fill the cache
// on every success and fall back to it when the network is unreachable, so
// offline reading keeps the shell it needs.
// Static assets. Pages link them as /static/...?v=<content hash> (the
// {{asset}} template func), so a versioned URL names exactly one set of bytes
// and is safe to serve cache-first: a redeploy that changes the file changes
// the URL. When one is cached, older versions of the same file are dropped so
// the shell cache doesn't grow by a copy per deploy.
//
// Anything requested by a bare URL (the precache list above, audio samples)
// stays network-first; the server sends those no-cache with a content-hash
// ETag, so the refetch is a 304 when nothing changed. Offline, either kind
// falls back to whatever copy of that path is cached, whatever its version.
if (url.pathname.indexOf("/static/") === 0) {
event.respondWith(
fetch(req).then(function (res) {
var versioned = url.searchParams.has("v");
var fromNetwork = function () {
return fetch(req).then(function (res) {
if (res && res.ok) {
var copy = res.clone();
caches.open(SHELL_CACHE).then(function (cache) { cache.put(req, copy); });
caches.open(SHELL_CACHE).then(function (cache) {
if (!versioned) return cache.put(req, copy);
return cache.keys().then(function (keys) {
keys.forEach(function (k) {
var ku = new URL(k.url);
if (ku.pathname === url.pathname && ku.search !== url.search) cache.delete(k);
});
return cache.put(req, copy);
});
});
}
return res;
}).catch(function () {
return caches.match(req);
})
return caches.match(req, { ignoreSearch: true });
});
};
event.respondWith(
versioned
? caches.match(req).then(function (hit) { return hit || fromNetwork(); })
: fromNetwork()
);
return;
}