perf enhancements
This commit is contained in:
@@ -131,15 +131,6 @@
|
|||||||
>
|
>
|
||||||
+ scroll to zoom
|
+ scroll to zoom
|
||||||
</span>
|
</span>
|
||||||
{#if zoomActive}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="flex cursor-pointer items-center gap-1.5 rounded-lg border border-primary/50 bg-primary/10 px-2.5 py-1 text-xs font-semibold text-primary transition-colors hover:bg-primary/15"
|
|
||||||
onclick={resetZoom}
|
|
||||||
>
|
|
||||||
Reset zoom
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex cursor-pointer items-center gap-1.5 rounded-lg border border-border bg-card px-3 py-1.5 text-xs font-semibold text-muted-foreground transition-colors hover:border-primary/50 hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
class="flex cursor-pointer items-center gap-1.5 rounded-lg border border-border bg-card px-3 py-1.5 text-xs font-semibold text-muted-foreground transition-colors hover:border-primary/50 hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
@@ -149,6 +140,17 @@
|
|||||||
>
|
>
|
||||||
{#if downloadingPng}Rendering…{:else}PNG{/if}
|
{#if downloadingPng}Rendering…{:else}PNG{/if}
|
||||||
</button>
|
</button>
|
||||||
|
<!-- Kept last so toggling it wraps onto a new line instead of shifting
|
||||||
|
the other controls (no layout shift on mobile). -->
|
||||||
|
{#if zoomActive}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex cursor-pointer items-center gap-1.5 rounded-lg border border-primary/50 bg-primary/10 px-2.5 py-1 text-xs font-semibold text-primary transition-colors hover:bg-primary/15"
|
||||||
|
onclick={resetZoom}
|
||||||
|
>
|
||||||
|
Reset zoom
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -154,7 +154,7 @@
|
|||||||
<div class="week-page">
|
<div class="week-page">
|
||||||
<div class="weather-content" style="min-height: 50vh">
|
<div class="weather-content" style="min-height: 50vh">
|
||||||
<!-- Page hero: prominent location + weather model selection -->
|
<!-- Page hero: prominent location + weather model selection -->
|
||||||
<div class="relative mb-3 flex flex-wrap items-center justify-between gap-x-6 gap-y-3 md:mb-5">
|
<div class="relative flex flex-wrap items-center justify-between gap-x-6 gap-y-3 md:mb-5">
|
||||||
<div class="flex min-w-0 items-center gap-3">
|
<div class="flex min-w-0 items-center gap-3">
|
||||||
<img
|
<img
|
||||||
class="h-10 w-10 shrink-0 rounded-full shadow-sm ring-2 ring-border"
|
class="h-10 w-10 shrink-0 rounded-full shadow-sm ring-2 ring-border"
|
||||||
|
|||||||
@@ -42,7 +42,6 @@
|
|||||||
let scrollParent: HTMLElement | Window | null = $state(null);
|
let scrollParent: HTMLElement | Window | null = $state(null);
|
||||||
|
|
||||||
const clamp = (v: number, lo = 0, hi = 1) => Math.min(hi, Math.max(lo, v));
|
const clamp = (v: number, lo = 0, hi = 1) => Math.min(hi, Math.max(lo, v));
|
||||||
const lerp = (a: number, b: number) => a + (b - a) * progress;
|
|
||||||
|
|
||||||
function findScrollParent(el: HTMLElement | null): HTMLElement | Window {
|
function findScrollParent(el: HTMLElement | null): HTMLElement | Window {
|
||||||
let node = el?.parentElement ?? null;
|
let node = el?.parentElement ?? null;
|
||||||
@@ -107,34 +106,21 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── Derived sizing ─────────────────────────────────────────────────────────
|
// All progress-driven sizing lives in CSS (via the single `--p` custom property
|
||||||
const CELL_W = 64;
|
// set on the strip), so a scroll frame writes ONE value instead of re-patching
|
||||||
// Full height fits the whole stack (weekday, label, icon, temps, precip+wind)
|
// height/padding/opacity inline styles on every cell.
|
||||||
// without clipping; the horizontal scroll container also clips vertically, so
|
|
||||||
// the content must fit inside the cell.
|
|
||||||
let cellH = $derived(lerp(134, 64));
|
|
||||||
let iconSize = $derived(lerp(40, 20));
|
|
||||||
// Top padding eases from pt-1.5 (6px) when full to pt-0.5 (2px) when compact.
|
|
||||||
let padTop = $derived(lerp(6, 1));
|
|
||||||
// The secondary rows collapse (height + opacity together) faster than the
|
|
||||||
// overall shrink, so the cell reaches a clean square before it stops shrinking.
|
|
||||||
let detailFactor = $derived(clamp(1 - progress * 1.6));
|
|
||||||
let relFactor = $derived(clamp(1 - progress * 2));
|
|
||||||
// Bar background/divider/shadow fade in promptly once it starts collapsing.
|
|
||||||
let chromeOpacity = $derived(clamp(progress / 0.35));
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- progress sentinel: 0-height marker just above the sticky strip -->
|
<!-- progress sentinel: 0-height marker just above the sticky strip -->
|
||||||
<div bind:this={sentinelEl} aria-hidden="true"></div>
|
<div bind:this={sentinelEl} aria-hidden="true"></div>
|
||||||
|
|
||||||
<div class="daystrip sticky -top-3 z-30 -mx-3 md:hidden" style="--chrome:{chromeOpacity}">
|
<div class="daystrip sticky -top-3 z-30 -mx-3 md:hidden" style="--p:{progress}">
|
||||||
<div class="flex gap-1.5 overflow-x-auto px-3 py-2" bind:this={stripScrollEl}>
|
<div class="flex gap-1.5 overflow-x-auto px-3 py-2" bind:this={stripScrollEl}>
|
||||||
{#if daily}
|
{#if daily}
|
||||||
{#if canExtendPast && onExtendPast}
|
{#if canExtendPast && onExtendPast}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="strip-side flex shrink-0 flex-col items-center justify-center rounded-xl border border-dashed border-border/70 text-muted-foreground"
|
class="strip-side flex shrink-0 flex-col items-center justify-center rounded-xl border border-dashed border-border/70 text-muted-foreground"
|
||||||
style="width:{CELL_W}px;height:{cellH}px"
|
|
||||||
onclick={onExtendPast}
|
onclick={onExtendPast}
|
||||||
aria-label="Load recent past days"
|
aria-label="Load recent past days"
|
||||||
>
|
>
|
||||||
@@ -166,7 +152,6 @@
|
|||||||
class="strip-cell flex shrink-0 cursor-pointer flex-col items-center justify-start gap-0.5 rounded-xl border px-1 pb-1.5 {selected
|
class="strip-cell flex shrink-0 cursor-pointer flex-col items-center justify-start gap-0.5 rounded-xl border px-1 pb-1.5 {selected
|
||||||
? 'border-primary bg-primary/10 ring-1 ring-primary/50'
|
? 'border-primary bg-primary/10 ring-1 ring-primary/50'
|
||||||
: 'border-border/60 bg-card'}"
|
: 'border-border/60 bg-card'}"
|
||||||
style="width:{CELL_W}px;height:{cellH}px;padding-top:{padTop}px"
|
|
||||||
aria-pressed={selected}
|
aria-pressed={selected}
|
||||||
onclick={() => onSelectDay(time, index)}
|
onclick={() => onSelectDay(time, index)}
|
||||||
>
|
>
|
||||||
@@ -176,25 +161,19 @@
|
|||||||
{formatZoned(time, daily.timezone, 'EEE').toUpperCase()}
|
{formatZoned(time, daily.timezone, 'EEE').toUpperCase()}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span
|
<span class="rel-label overflow-hidden text-[9px] leading-none text-muted-foreground">
|
||||||
class="overflow-hidden text-[9px] leading-none text-muted-foreground"
|
|
||||||
style="opacity:{relFactor};max-height:{relFactor * 14}px"
|
|
||||||
>
|
|
||||||
{getRelativeDayLabel(time, daily.timezone)}
|
{getRelativeDayLabel(time, daily.timezone)}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<svg class="fill-foreground" width="{iconSize}px" height="{iconSize}px">
|
<svg class="day-icon fill-foreground">
|
||||||
<use
|
<use
|
||||||
xlink:href="/images/weather-icons/{getWeatherIconName(wCode, true)}.svg#Layer_1"
|
xlink:href="/images/weather-icons/{getWeatherIconName(wCode, true)}.svg#Layer_1"
|
||||||
></use>
|
></use>
|
||||||
</svg>
|
</svg>
|
||||||
<!-- night companion icon, present in the full view, fades as it collapses -->
|
<!-- night companion icon, present in the full view, fades as it collapses -->
|
||||||
<svg
|
<svg
|
||||||
class="absolute -right-1 -bottom-0.5 rounded-full bg-card fill-foreground/60 p-px ring-1 ring-border/60"
|
class="night-badge absolute -right-1 -bottom-0.5 rounded-full bg-card fill-foreground/60 p-px ring-1 ring-border/60"
|
||||||
width="{iconSize * 0.44}px"
|
|
||||||
height="{iconSize * 0.44}px"
|
|
||||||
style="opacity:{relFactor}"
|
|
||||||
>
|
>
|
||||||
<use
|
<use
|
||||||
xlink:href="/images/weather-icons/{getWeatherIconName(
|
xlink:href="/images/weather-icons/{getWeatherIconName(
|
||||||
@@ -218,8 +197,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex w-full flex-col items-center gap-0.5 overflow-hidden text-[10px] tabular-nums text-muted-foreground"
|
class="detail-row flex w-full flex-col items-center gap-0.5 overflow-hidden text-[10px] tabular-nums text-muted-foreground"
|
||||||
style="opacity:{detailFactor};max-height:{detailFactor * 42}px"
|
|
||||||
>
|
>
|
||||||
<span class="inline-flex items-center gap-1">
|
<span class="inline-flex items-center gap-1">
|
||||||
<svg class="fill-sky-500" width="13" height="13">
|
<svg class="fill-sky-500" width="13" height="13">
|
||||||
@@ -242,7 +220,6 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="strip-side flex shrink-0 flex-col items-center justify-center rounded-xl border border-dashed border-border/70 text-muted-foreground"
|
class="strip-side flex shrink-0 flex-col items-center justify-center rounded-xl border border-dashed border-border/70 text-muted-foreground"
|
||||||
style="width:{CELL_W}px;height:{cellH}px"
|
|
||||||
onclick={onExtend}
|
onclick={onExtend}
|
||||||
aria-label="Load the longer-range forecast"
|
aria-label="Load the longer-range forecast"
|
||||||
>
|
>
|
||||||
@@ -264,9 +241,30 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* The bar background/divider/shadow fade in only once the strip has collapsed
|
/* Everything below is derived from a single custom property `--p` (0 = full
|
||||||
into its compact form, so at the top of the page it reads as plain cards. */
|
cards, 1 = compact strip) set on `.daystrip` once per scroll frame. The
|
||||||
|
browser resolves the calc()s natively, so a scroll frame is one property
|
||||||
|
write instead of dozens of inline-style patches across every cell. */
|
||||||
.daystrip {
|
.daystrip {
|
||||||
|
/* tunables */
|
||||||
|
--cell-w: 64px;
|
||||||
|
--cell-h-full: 134px;
|
||||||
|
--cell-h-min: 64px;
|
||||||
|
--icon-full: 40px;
|
||||||
|
--icon-min: 20px;
|
||||||
|
--pt-full: 6px;
|
||||||
|
--pt-min: 1px;
|
||||||
|
|
||||||
|
/* progress-derived (--k is the "fullness": 1 when full, 0 when compact) */
|
||||||
|
--k: calc(1 - var(--p));
|
||||||
|
--cell-h: calc(var(--cell-h-min) + (var(--cell-h-full) - var(--cell-h-min)) * var(--k));
|
||||||
|
--icon: calc(var(--icon-min) + (var(--icon-full) - var(--icon-min)) * var(--k));
|
||||||
|
--pt: calc(var(--pt-min) + (var(--pt-full) - var(--pt-min)) * var(--k));
|
||||||
|
--rel: clamp(0, calc(1 - var(--p) * 2), 1);
|
||||||
|
--detail: clamp(0, calc(1 - var(--p) * 1.6), 1);
|
||||||
|
/* bar background/divider/shadow fade in once it starts collapsing */
|
||||||
|
--chrome: clamp(0, calc(var(--p) / 0.35), 1);
|
||||||
|
|
||||||
background: color-mix(
|
background: color-mix(
|
||||||
in oklab,
|
in oklab,
|
||||||
var(--color-background) calc(var(--chrome) * 100%),
|
var(--color-background) calc(var(--chrome) * 100%),
|
||||||
@@ -277,12 +275,37 @@
|
|||||||
color-mix(in oklab, var(--color-border) calc(var(--chrome) * 100%), transparent);
|
color-mix(in oklab, var(--color-border) calc(var(--chrome) * 100%), transparent);
|
||||||
box-shadow: 0 6px 12px -8px rgba(0, 0, 0, calc(var(--chrome) * 0.35));
|
box-shadow: 0 6px 12px -8px rgba(0, 0, 0, calc(var(--chrome) * 0.35));
|
||||||
}
|
}
|
||||||
|
|
||||||
.strip-cell,
|
.strip-cell,
|
||||||
.strip-side {
|
.strip-side {
|
||||||
|
width: var(--cell-w);
|
||||||
|
height: var(--cell-h);
|
||||||
|
/* size tracks scroll exactly (no transition); only the tap highlight eases */
|
||||||
transition:
|
transition:
|
||||||
border-color 0.15s,
|
border-color 0.15s,
|
||||||
background-color 0.15s;
|
background-color 0.15s;
|
||||||
}
|
}
|
||||||
|
.strip-cell {
|
||||||
|
padding-top: var(--pt);
|
||||||
|
}
|
||||||
|
.day-icon {
|
||||||
|
width: var(--icon);
|
||||||
|
height: var(--icon);
|
||||||
|
}
|
||||||
|
.night-badge {
|
||||||
|
width: calc(var(--icon) * 0.44);
|
||||||
|
height: calc(var(--icon) * 0.44);
|
||||||
|
opacity: var(--rel);
|
||||||
|
}
|
||||||
|
.rel-label {
|
||||||
|
opacity: var(--rel);
|
||||||
|
max-height: calc(14px * var(--rel));
|
||||||
|
}
|
||||||
|
.detail-row {
|
||||||
|
opacity: var(--detail);
|
||||||
|
max-height: calc(42px * var(--detail));
|
||||||
|
}
|
||||||
|
|
||||||
.daystrip :global(.overflow-x-auto) {
|
.daystrip :global(.overflow-x-auto) {
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,25 +192,6 @@
|
|||||||
>
|
>
|
||||||
+ scroll to zoom
|
+ scroll to zoom
|
||||||
</span>
|
</span>
|
||||||
{#if zoomActive}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="flex cursor-pointer items-center gap-1.5 rounded-lg border border-primary/50 bg-primary/10 px-2.5 py-1 text-xs font-semibold text-primary transition-colors hover:bg-primary/15"
|
|
||||||
onclick={resetZoom}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="h-3.5 w-3.5"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="2"
|
|
||||||
>
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v6h6M20 20v-6h-6" />
|
|
||||||
<path stroke-linecap="round" d="M20 10a8 8 0 0 0-14-4M4 14a8 8 0 0 0 14 4" />
|
|
||||||
</svg>
|
|
||||||
Reset zoom
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
<div
|
<div
|
||||||
class="inline-flex items-center rounded-lg bg-muted p-0.5 text-xs font-semibold"
|
class="inline-flex items-center rounded-lg bg-muted p-0.5 text-xs font-semibold"
|
||||||
role="group"
|
role="group"
|
||||||
@@ -276,6 +257,27 @@
|
|||||||
{/if}
|
{/if}
|
||||||
PNG
|
PNG
|
||||||
</button>
|
</button>
|
||||||
|
<!-- Kept last so toggling it wraps onto a new line instead of shifting
|
||||||
|
the other controls (no layout shift on mobile). -->
|
||||||
|
{#if zoomActive}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex cursor-pointer items-center gap-1.5 rounded-lg border border-primary/50 bg-primary/10 px-2.5 py-1 text-xs font-semibold text-primary transition-colors hover:bg-primary/15"
|
||||||
|
onclick={resetZoom}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="h-3.5 w-3.5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v6h6M20 20v-6h-6" />
|
||||||
|
<path stroke-linecap="round" d="M20 10a8 8 0 0 0-14-4M4 14a8 8 0 0 0 14 4" />
|
||||||
|
</svg>
|
||||||
|
Reset zoom
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user