minor adjustments
This commit is contained in:
@@ -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();
|
||||||
@@ -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();
|
|
||||||
@@ -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
@@ -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();
|
||||||
@@ -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()}
|
||||||
|
|||||||
@@ -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,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 />
|
||||||
|
|||||||
@@ -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}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
|
||||||
Reference in New Issue
Block a user