Files
Pete/internal/web/templates/layout.html
prosolis 1a43616248 Add local weather forecast with live-weather backgrounds
Visitors can save a postal code (international, via Zippopotam) to get the
local forecast from Open-Meteo — a header chip + a 5-day home-page card —
and the canvas background switches from the seasonal effect to live
conditions. Entirely client-side: no keys, no server logic. Geocode cached
permanently, forecast cached 2h. Celsius by default, Fahrenheit opt-in.

New canvas effects: clear (sun by day, shaded moon + stars at night),
clouds (blurred drifting sprites), snow, fog, and storm (rain + lightning).
Seasonal effects remain the no-location fallback.
2026-06-21 00:24:39 -07:00

158 lines
10 KiB
HTML

{{define "layout"}}<!doctype html>
<html lang="en" data-phase="{{if .Phase}}{{.Phase}}{{else}}day{{end}}"{{if .Phase}} data-phase-lock{{end}} data-season="{{.Weather.Season}}" data-weather="{{.Weather.Variant}}" data-intensity="{{.Weather.Intensity}}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{block "title" .}}{{.SiteTitle}}{{end}}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&family=Nunito:wght@400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/css/output.css">
<link rel="icon" href="/static/img/pete.avif" type="image/avif">
<script>
// Pick a palette phase from the browser clock and update it each minute.
(function () {
function phase(h) {
if (h >= 5 && h < 8) return "dawn";
if (h >= 8 && h < 17) return "day";
if (h >= 17 && h < 20) return "dusk";
return "night";
}
function apply() {
if (document.documentElement.hasAttribute("data-phase-lock")) return;
document.documentElement.dataset.phase = phase(new Date().getHours());
}
apply();
setInterval(apply, 60000);
})();
</script>
</head>
<body class="min-h-screen bg-[color:var(--bg)] text-[color:var(--ink)] transition-colors duration-1000">
<div class="pointer-events-none fixed inset-0 -z-10 bg-[color:var(--bg-grad)] transition-colors duration-1000"></div>
<canvas id="pete-weather" class="pointer-events-none fixed inset-0 -z-[5] h-full w-full" aria-hidden="true"></canvas>
<header class="mx-auto max-w-6xl px-4 pt-8 pb-4 sm:pt-12">
<div class="flex flex-wrap items-center justify-between gap-3 sm:gap-4">
<a href="/" class="flex items-center gap-3 group min-w-0">
<img src="/static/img/pete.avif" alt="Pete" width="48" height="48"
class="h-12 w-12 shrink-0 rounded-2xl bg-[color:var(--card)] shadow-pete border-2 border-[color:var(--ink)]/10 object-cover group-hover:-rotate-6 transition-transform">
<span class="font-display text-3xl font-bold tracking-tight">{{.SiteTitle}}</span>
<span class="hidden sm:inline rounded-full bg-[color:var(--accent)]/20 px-3 py-1 text-xs font-semibold uppercase tracking-wider text-[color:var(--ink)]/70" data-phase-label>day</span>
</a>
<button type="button" data-settings-trigger
title="Feed settings"
class="inline-flex shrink-0 items-center justify-center rounded-full bg-[color:var(--card)] p-2 shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition">
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h0a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51h0a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v0a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg>
<span class="sr-only">Feed settings</span>
</button>
{{if .Weather.Variant}}
<button type="button" data-weather-toggle aria-pressed="true"
title="Toggle weather animation"
class="inline-flex shrink-0 items-center justify-center rounded-full bg-[color:var(--card)] p-2 shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition">
<svg aria-hidden="true" viewBox="0 0 24 24" class="h-5 w-5 transition-colors"
data-weather-star fill="currentColor" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round">
<polygon points="12,2.7 14.85,9.27 21.95,10.05 16.6,14.83 18.18,21.8 12,18.13 5.82,21.8 7.4,14.83 2.05,10.05 9.15,9.27"/>
</svg>
<span class="sr-only">Toggle weather animation</span>
</button>
{{end}}
<button type="button" data-weather-loc
title="Set your location"
class="inline-flex shrink-0 items-center gap-1.5 rounded-full bg-[color:var(--card)] px-3 py-2 text-sm font-semibold shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition">
<span data-weather-chip class="tabular-nums">📍 Weather</span>
</button>
<div class="flex items-center gap-2 min-w-0 max-w-full">
<button type="button" data-search-trigger title="Search (⌘K)"
class="flex shrink-0 items-center gap-2 rounded-full bg-[color:var(--card)] px-3 py-2 text-sm shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition">
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4 text-[color:var(--ink)]/60">
<circle cx="11" cy="11" r="7"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<span class="hidden sm:inline text-[color:var(--ink)]/60 font-semibold">Search</span>
<kbd class="hidden sm:inline-flex items-center rounded-md bg-[color:var(--ink)]/5 px-1.5 py-0.5 text-[10px] font-mono font-semibold text-[color:var(--ink)]/60">⌘K</kbd>
</button>
<nav class="pete-channel-nav flex min-w-0 items-center gap-1 sm:gap-2 overflow-x-auto rounded-full bg-[color:var(--card)] p-1 shadow-pete border-2 border-[color:var(--ink)]/10">
{{$active := .Active}}
{{range .Channels}}
<a href="/{{.Slug}}"
class="shrink-0 rounded-full px-3 py-2 text-sm font-semibold transition
{{if eq $active .Slug}}bg-theme-{{.Theme}} text-white shadow-sm{{else}}hover:bg-[color:var(--ink)]/5{{end}}">
<span aria-hidden="true" class="mr-1">{{.Emoji}}</span><span class="hidden sm:inline">{{.Title}}</span>
</a>
{{end}}
</nav>
</div>
</div>
</header>
<div id="pete-search" class="hidden fixed inset-0 z-50 bg-[color:var(--ink)]/40 backdrop-blur-sm flex items-start justify-center pt-[10vh] px-4" aria-modal="true" role="dialog">
<div class="w-full max-w-2xl rounded-3xl bg-[color:var(--card)] shadow-pete-lg border-2 border-[color:var(--ink)]/10 overflow-hidden">
<div class="flex items-center gap-3 px-5 py-4 border-b border-[color:var(--ink)]/10">
<kbd class="hidden sm:inline-flex items-center rounded-md bg-[color:var(--ink)]/5 px-1.5 py-0.5 text-[10px] font-mono font-semibold text-[color:var(--ink)]/60">⌘K</kbd>
<input type="search" data-search-input autocomplete="off" spellcheck="false"
placeholder="Search headlines…"
class="flex-1 bg-transparent text-lg outline-none placeholder:text-[color:var(--ink)]/40">
</div>
<div data-search-list class="max-h-[60vh] overflow-y-auto p-3 space-y-1"></div>
<div class="px-5 py-3 border-t border-[color:var(--ink)]/10 flex items-center justify-between text-xs text-[color:var(--ink)]/50">
<span><kbd class="font-mono">↑↓</kbd> navigate · <kbd class="font-mono"></kbd> open · <kbd class="font-mono">esc</kbd> close</span>
<span data-search-meta></span>
</div>
</div>
</div>
<div id="pete-settings" class="hidden fixed inset-0 z-50 bg-[color:var(--ink)]/40 backdrop-blur-sm flex items-start justify-center pt-[10vh] px-4" aria-modal="true" role="dialog">
<div class="w-full max-w-md 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">
<h2 class="font-display text-lg font-bold">Feed settings</h2>
<button type="button" data-settings-close class="rounded-full px-2 py-1 text-sm text-[color:var(--ink)]/60 hover:bg-[color:var(--ink)]/5">esc</button>
</div>
<p class="px-5 pt-3 text-xs text-[color:var(--ink)]/60">Uncheck a feed to hide its stories. Saved in this browser.</p>
<div data-settings-list class="max-h-[55vh] overflow-y-auto p-3 space-y-1"></div>
<div class="px-5 py-3 border-t border-[color:var(--ink)]/10 flex items-center justify-between text-xs">
<button type="button" data-settings-reset class="text-[color:var(--ink)]/60 hover:text-[color:var(--ink)] font-semibold">Enable all</button>
<span data-settings-meta class="text-[color:var(--ink)]/50"></span>
</div>
</div>
</div>
<main class="mx-auto max-w-6xl px-4 pb-24">
{{block "main" .}}{{end}}
</main>
<footer class="mx-auto max-w-6xl px-4 pb-10 text-center text-sm text-[color:var(--ink)]/50">
<img src="/static/img/pete.avif" alt="" width="20" height="20"
class="inline-block h-5 w-5 rounded-full object-cover align-[-4px] mr-1">
served by <span class="font-semibold">Pete</span> · also a Matrix bot · <span data-clock></span>
</footer>
<script>
// Tiny clock + phase label updater for the header pill / footer.
(function () {
function tick() {
var now = new Date();
var hh = String(now.getHours()).padStart(2, "0");
var mm = String(now.getMinutes()).padStart(2, "0");
var el = document.querySelector("[data-clock]");
if (el) el.textContent = hh + ":" + mm;
var label = document.querySelector("[data-phase-label]");
if (label) label.textContent = document.documentElement.dataset.phase;
}
tick();
setInterval(tick, 30000);
})();
</script>
<script>window.PETE_SOURCES = {{.AllSources}};</script>
<script src="/static/js/weather.js" defer></script>
<script src="/static/js/weather-forecast.js" defer></script>
<script src="/static/js/search.js" defer></script>
<script src="/static/js/settings.js" defer></script>
</body>
</html>{{end}}