minor adjustments

This commit is contained in:
Vincent van der Wal
2026-08-01 16:07:32 +02:00
parent 17cf8df109
commit ae88140f79
13 changed files with 129 additions and 122 deletions
+20
View File
@@ -0,0 +1,20 @@
import { chromium } from 'playwright';
const browser = await chromium.launch();
for (const [label, supporter] of [['plain', false], ['supporter', true]]) {
const ctx = await browser.newContext({ viewport: { width: 1280, height: 950 } });
if (supporter) await ctx.addInitScript(() => localStorage.setItem('supporter_cache_v1', JSON.stringify({ valid: true, expires: null, checkedAt: 1785578719118 })));
const page = await ctx.newPage();
await page.goto('http://localhost:5186/en/weather/week/berlin/', { waitUntil: 'load', timeout: 120000 });
await page.waitForTimeout(9000);
const btn = page.getByRole('button', { name: /Support Drizz|Supporter extras/ }).first();
await btn.screenshot({ path: `/tmp/badge-${label}.png` });
await ctx.close();
}
// locked panel
const ctx = await browser.newContext({ viewport: { width: 1280, height: 950 } });
const page = await ctx.newPage();
await page.goto('http://localhost:5186/en/weather/seasonal/berlin/', { waitUntil: 'load', timeout: 120000 });
await page.waitForTimeout(6000);
await page.locator('h2').first().locator('xpath=..').screenshot({ path: '/tmp/badge-panel.png' }).catch(() => {});
console.log('ok');
await browser.close();
-25
View File
@@ -1,25 +0,0 @@
import { chromium } from 'playwright';
const browser = await chromium.launch();
const out = {};
for (const [label, vp, mobile] of [['mobile', { width: 390, height: 844 }, true], ['desktop', { width: 1280, height: 950 }, false]]) {
const ctx = await browser.newContext({ viewport: vp, isMobile: mobile, hasTouch: mobile });
const page = await ctx.newPage();
await page.goto('http://localhost:5185/en/weather/week/berlin/', { waitUntil: 'load', timeout: 120000 });
await page.waitForTimeout(14000);
await page.locator('canvas').first().scrollIntoViewIfNeeded();
await page.waitForTimeout(1500);
out[label] = await page.evaluate(() => {
const c = document.querySelector('.strip-cell').getBoundingClientRect();
const icon = document.querySelector('.strip-cell .day-icon').getBoundingClientRect();
const temps = document.querySelector('.strip-cell .temp-max').getBoundingClientRect();
return {
cell: { w: Math.round(c.width), h: Math.round(c.height) },
iconOffCentre: +(icon.top + icon.height / 2 - (c.top + c.height / 2)).toFixed(1),
gapBelowTemps: +(c.bottom - temps.bottom).toFixed(1)
};
});
await page.screenshot({ path: `/tmp/gap-${label}.png`, clip: { x: 0, y: 0, width: vp.width, height: 200 } });
await ctx.close();
}
console.log(JSON.stringify(out, null, 1));
await browser.close();
+20
View File
@@ -0,0 +1,20 @@
import { chromium } from 'playwright';
const browser = await chromium.launch();
const out = {};
for (const [label, vp, mobile] of [['mobile', { width: 390, height: 844 }, true], ['desktop', { width: 1280, height: 950 }, false]]) {
const ctx = await browser.newContext({ viewport: vp, isMobile: mobile, hasTouch: mobile });
const page = await ctx.newPage();
await page.goto('http://localhost:5186/en/weather/week/berlin/', { waitUntil: 'load', timeout: 120000 });
await page.waitForTimeout(14000);
out[label] = await page.evaluate(() => ({
rows: [...document.querySelectorAll('tr')].map((r) => ({
label: r.querySelector('.hdr')?.textContent?.trim().replace(/\s+/g, ' ').slice(0, 10) || '-',
h: Math.round(r.getBoundingClientRect().height)
})),
tableHeight: Math.round(document.querySelector('table').getBoundingClientRect().height)
}));
await page.screenshot({ path: `/tmp/rows-${label}.png`, clip: { x: 0, y: 0, width: vp.width, height: Math.min(820, vp.height) } });
await ctx.close();
}
console.log(JSON.stringify(out, null, 1));
await browser.close();
+15
View File
@@ -0,0 +1,15 @@
import { chromium } from 'playwright';
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1280, height: 950 } });
await page.goto('http://localhost:5186/en/weather/week/berlin/', { waitUntil: 'load', timeout: 120000 });
await page.waitForTimeout(14000);
// scroll right down to the footer and click a link there
await page.evaluate(() => { const m = document.querySelector('main'); m.scrollTop = m.scrollHeight; });
await page.waitForTimeout(700);
const before = await page.evaluate(() => document.querySelector('main').scrollTop);
await page.locator('footer a[href*="/weather/14-day"]').first().click();
await page.waitForTimeout(7000);
const after = await page.evaluate(() => document.querySelector('main').scrollTop);
console.log(JSON.stringify({ scrollBeforeClick: Math.round(before), scrollAfterNav: Math.round(after), url: page.url() }));
await browser.close();
+3 -3
View File
@@ -144,15 +144,15 @@
<!-- md+: the same settings as individual controls. Kept mounted (not `{#if}`) <!-- md+: the same settings as individual controls. Kept mounted (not `{#if}`)
so SupporterBadge still verifies the key on every viewport. --> so SupporterBadge still verifies the key on every viewport. -->
<div class="hidden items-center gap-3 md:flex"> <div class="hidden items-center gap-3 md:flex">
<!-- Supporter status / unlock -->
<SupporterBadge />
<!-- Language: the locale lives in the URL, so this is a set of links --> <!-- Language: the locale lives in the URL, so this is a set of links -->
<LanguageSelector /> <LanguageSelector />
<!-- Measurement units --> <!-- Measurement units -->
<UnitSelector /> <UnitSelector />
<!-- Supporter status / unlock -->
<SupporterBadge />
<!-- Theme toggle: system → light → dark --> <!-- Theme toggle: system → light → dark -->
<button <button
class="flex h-9 w-9 shrink-0 cursor-pointer items-center justify-center rounded-full border border-border/70 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground" class="flex h-9 w-9 shrink-0 cursor-pointer items-center justify-center rounded-full border border-border/70 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
@@ -6,6 +6,7 @@
import UnitOptions from '$lib/components/unit-options.svelte'; import UnitOptions from '$lib/components/unit-options.svelte';
import * as m from '$lib/paraglide/messages'; import * as m from '$lib/paraglide/messages';
import SupporterIcon from '$lib/paywall/SupporterIcon.svelte';
import UnlockDialog from '$lib/paywall/UnlockDialog.svelte'; import UnlockDialog from '$lib/paywall/UnlockDialog.svelte';
import { isSupporter } from '$lib/paywall/supporter'; import { isSupporter } from '$lib/paywall/supporter';
@@ -84,32 +85,10 @@
unlockOpen = true; unlockOpen = true;
}} }}
> >
{#if $isSupporter} <SupporterIcon
<!-- star --> filled={$isSupporter}
<svg class="h-4.5 w-4.5 shrink-0 {$isSupporter ? 'text-amber-500' : 'text-muted-foreground'}"
class="h-4.5 w-4.5 shrink-0 text-amber-500" />
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
>
<path
d="M12 2.5l2.9 6 6.6.9-4.8 4.6 1.2 6.5L12 17.9 6.1 20.5l1.2-6.5L2.5 9.4l6.6-.9L12 2.5z"
/>
</svg>
{:else}
<!-- padlock -->
<svg
class="h-4.5 w-4.5 shrink-0 text-muted-foreground"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
stroke-width="1.75"
aria-hidden="true"
>
<rect x="4" y="10" width="16" height="11" rx="2" />
<path stroke-linecap="round" d="M8 10V7a4 4 0 0 1 8 0v3" />
</svg>
{/if}
<span class="min-w-0 flex-1"> <span class="min-w-0 flex-1">
<span class="block text-[13px] font-semibold"> <span class="block text-[13px] font-semibold">
{$isSupporter ? m.supporter_active() : m.supporter_support()} {$isSupporter ? m.supporter_active() : m.supporter_support()}
+2 -11
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import SupporterIcon from './SupporterIcon.svelte';
import UnlockDialog from './UnlockDialog.svelte'; import UnlockDialog from './UnlockDialog.svelte';
import { SIGNUP_URL, SUPPORTER_PERKS, getSupporterPrice } from './config'; import { SIGNUP_URL, SUPPORTER_PERKS, getSupporterPrice } from './config';
import { isSupporter, refreshSupporter, supporterState } from './supporter'; import { isSupporter, refreshSupporter, supporterState } from './supporter';
@@ -39,17 +40,7 @@
<div <div
class="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-2xl bg-primary/10 text-primary" class="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-2xl bg-primary/10 text-primary"
> >
<!-- padlock --> <SupporterIcon class="h-7 w-7" />
<svg
class="h-7 w-7"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
stroke-width="1.75"
>
<rect x="4" y="10" width="16" height="11" rx="2" />
<path stroke-linecap="round" d="M8 10V7a4 4 0 0 1 8 0v3" />
</svg>
</div> </div>
<h2 class="text-xl font-bold tracking-tight">{feature} is a supporter extra</h2> <h2 class="text-xl font-bold tracking-tight">{feature} is a supporter extra</h2>
+3 -23
View File
@@ -3,6 +3,7 @@
import * as m from '$lib/paraglide/messages'; import * as m from '$lib/paraglide/messages';
import SupporterIcon from './SupporterIcon.svelte';
import UnlockDialog from './UnlockDialog.svelte'; import UnlockDialog from './UnlockDialog.svelte';
import { isSupporter, refreshSupporter } from './supporter'; import { isSupporter, refreshSupporter } from './supporter';
@@ -21,29 +22,8 @@
title={$isSupporter ? m.supporter_active() : m.supporter_support()} title={$isSupporter ? m.supporter_active() : m.supporter_support()}
aria-label={$isSupporter ? m.supporter_active() : m.supporter_support()} aria-label={$isSupporter ? m.supporter_active() : m.supporter_support()}
> >
{#if $isSupporter} <SupporterIcon filled={$isSupporter} />
<!-- star --> <span class="hidden sm:inline">{m.supporter_badge()}</span>
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path
d="M12 2.5l2.9 6 6.6.9-4.8 4.6 1.2 6.5L12 17.9 6.1 20.5l1.2-6.5L2.5 9.4l6.6-.9L12 2.5z"
/>
</svg>
<span class="hidden sm:inline">{m.supporter_badge()}</span>
{:else}
<!-- padlock -->
<svg
class="h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
stroke-width="1.75"
aria-hidden="true"
>
<rect x="4" y="10" width="16" height="11" rx="2" />
<path stroke-linecap="round" d="M8 10V7a4 4 0 0 1 8 0v3" />
</svg>
<span class="hidden sm:inline">{m.supporter_badge()}</span>
{/if}
</button> </button>
<UnlockDialog bind:open /> <UnlockDialog bind:open />
+37
View File
@@ -0,0 +1,37 @@
<script lang="ts">
/**
* The supporter mark. A heart rather than a padlock: nothing here is locked
* away as a punishment - the extras are a thank-you for chipping in, and a
* padlock framed it as a paywall. Filled once someone is supporting,
* outlined as an invitation before that.
*/
interface Props {
filled?: boolean;
class?: string;
}
let { filled = false, class: className = 'h-4 w-4' }: Props = $props();
</script>
{#if filled}
<svg class={className} viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path
d="M12 21.1l-1.45-1.32C5.4 15.1 2 12 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.5-3.4 6.6-8.55 11.29L12 21.1z"
/>
</svg>
{:else}
<svg
class={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.9"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path
d="M12 21.1l-1.45-1.32C5.4 15.1 2 12 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.5-3.4 6.6-8.55 11.29L12 21.1z"
/>
</svg>
{/if}
+11 -1
View File
@@ -126,7 +126,16 @@
return new Promise((resolve) => setTimeout(resolve, FADE_OUT_MS)); return new Promise((resolve) => setTimeout(resolve, FADE_OUT_MS));
}); });
afterNavigate(() => { let mainEl = $state<HTMLElement | null>(null);
afterNavigate((navigation) => {
// The page scrolls inside <main>, not the window, so SvelteKit's own scroll
// handling never touches it and a new page would open half way down.
// Back/forward and in-page anchors keep their position.
if (navigation.type !== 'popstate' && !navigation.to?.url.hash) {
mainEl?.scrollTo({ top: 0 });
}
if (typeof document.startViewTransition === 'function' || reducedMotion()) { if (typeof document.startViewTransition === 'function' || reducedMotion()) {
contentVisible = true; contentVisible = true;
return; return;
@@ -202,6 +211,7 @@
flex-col + flex-1 below keeps the footer on the bottom edge even when flex-col + flex-1 below keeps the footer on the bottom edge even when
the page is too short to fill the viewport. --> the page is too short to fill the viewport. -->
<main <main
bind:this={mainEl}
class={fullBleed class={fullBleed
? 'flex-1 overflow-hidden' ? 'flex-1 overflow-hidden'
: 'flex flex-1 flex-col overflow-y-auto p-3 lg:px-8 lg:py-6'} : 'flex flex-1 flex-col overflow-y-auto p-3 lg:px-8 lg:py-6'}
@@ -472,7 +472,7 @@
--night-icon-nudge: 4px; --night-icon-nudge: 4px;
/* compact only: signed shift that lands the lone icon dead centre in the /* compact only: signed shift that lands the lone icon dead centre in the
square tile (measured against the tile's mid-line) */ square tile (measured against the tile's mid-line) */
--icon-lift: -1px; --icon-lift: 1px;
/* cell bottom padding (roomy when full, a little breathing room under the /* cell bottom padding (roomy when full, a little breathing room under the
temps when compact so they don't sit on the tile edge). The compact temps when compact so they don't sit on the tile edge). The compact
tile is taller than its content here, so the leftover slack already tile is taller than its content here, so the leftover slack already
@@ -540,7 +540,7 @@
--tmax-padx-min: 6px; --tmax-padx-min: 6px;
--pt-min: 5px; --pt-min: 5px;
--pb-min: 6px; --pb-min: 6px;
--icon-lift: 0px; --icon-lift: 1px;
} }
} }
@@ -899,16 +899,24 @@
padding: 1px; padding: 1px;
font-size: 10px; font-size: 10px;
} }
/* The row-header icon and the fixed cell height are what actually set the /* The row-header icon was setting the height of every row it sat in - the
row height, so both come down explicitly. */ stacked icon + label + unit needed ~52px while the value cells only
.cell :global(svg), needed 34. The label already names the row, so on phones the icon goes
and the cells decide the height. */
.hdr :global(svg) { .hdr :global(svg) {
display: none;
}
.cell :global(svg) {
width: 22px; width: 22px;
height: 22px; height: 22px;
} }
.cell { .cell {
height: 34px; height: 34px;
} }
/* except precipitation, whose cell is a bar chart and needs the room */
.precip-cell {
height: 46px;
}
.hdr :global(span) { .hdr :global(span) {
font-size: 9px; font-size: 9px;
line-height: 1.15; line-height: 1.15;
-28
View File
@@ -1,28 +0,0 @@
import { chromium } from 'playwright';
const browser = await chromium.launch();
const out = {};
for (const [label, vp] of [['desktop', { width: 1280, height: 950 }], ['tablet', { width: 900, height: 800 }]]) {
const ctx = await browser.newContext({ viewport: vp });
const page = await ctx.newPage();
await page.goto('http://localhost:5185/en/weather/week/berlin/', { waitUntil: 'load', timeout: 120000 });
await page.waitForTimeout(14000);
await page.locator('canvas').first().scrollIntoViewIfNeeded();
await page.waitForTimeout(1500);
out[label] = await page.evaluate(() => {
const c = document.querySelector('.strip-cell').getBoundingClientRect();
const icon = document.querySelector('.strip-cell .day-icon').getBoundingClientRect();
const temps = document.querySelector('.strip-cell .temp-max').getBoundingClientRect();
const canvas = document.querySelector('canvas');
const cw = canvas ? canvas.getBoundingClientRect() : null;
return {
cell: { w: Math.round(c.width), h: Math.round(c.height) },
iconOffCentre: +(icon.top + icon.height / 2 - (c.top + c.height / 2)).toFixed(1),
gapBelowTemps: +(c.bottom - temps.bottom).toFixed(1),
canvasWidth: cw ? Math.round(cw.width) : null
};
});
await page.screenshot({ path: `/tmp/tiles-${label}.png`, clip: { x: 0, y: 0, width: vp.width, height: 220 } });
await ctx.close();
}
console.log(JSON.stringify(out, null, 1));
await browser.close();