perf enhancements
This commit is contained in:
@@ -42,7 +42,6 @@
|
||||
let scrollParent: HTMLElement | Window | null = $state(null);
|
||||
|
||||
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 {
|
||||
let node = el?.parentElement ?? null;
|
||||
@@ -107,34 +106,21 @@
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Derived sizing ─────────────────────────────────────────────────────────
|
||||
const CELL_W = 64;
|
||||
// Full height fits the whole stack (weekday, label, icon, temps, precip+wind)
|
||||
// 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));
|
||||
// All progress-driven sizing lives in CSS (via the single `--p` custom property
|
||||
// set on the strip), so a scroll frame writes ONE value instead of re-patching
|
||||
// height/padding/opacity inline styles on every cell.
|
||||
</script>
|
||||
|
||||
<!-- progress sentinel: 0-height marker just above the sticky strip -->
|
||||
<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}>
|
||||
{#if daily}
|
||||
{#if canExtendPast && onExtendPast}
|
||||
<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"
|
||||
style="width:{CELL_W}px;height:{cellH}px"
|
||||
onclick={onExtendPast}
|
||||
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
|
||||
? 'border-primary bg-primary/10 ring-1 ring-primary/50'
|
||||
: 'border-border/60 bg-card'}"
|
||||
style="width:{CELL_W}px;height:{cellH}px;padding-top:{padTop}px"
|
||||
aria-pressed={selected}
|
||||
onclick={() => onSelectDay(time, index)}
|
||||
>
|
||||
@@ -176,25 +161,19 @@
|
||||
{formatZoned(time, daily.timezone, 'EEE').toUpperCase()}
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="overflow-hidden text-[9px] leading-none text-muted-foreground"
|
||||
style="opacity:{relFactor};max-height:{relFactor * 14}px"
|
||||
>
|
||||
<span class="rel-label overflow-hidden text-[9px] leading-none text-muted-foreground">
|
||||
{getRelativeDayLabel(time, daily.timezone)}
|
||||
</span>
|
||||
|
||||
<div class="relative">
|
||||
<svg class="fill-foreground" width="{iconSize}px" height="{iconSize}px">
|
||||
<svg class="day-icon fill-foreground">
|
||||
<use
|
||||
xlink:href="/images/weather-icons/{getWeatherIconName(wCode, true)}.svg#Layer_1"
|
||||
></use>
|
||||
</svg>
|
||||
<!-- night companion icon, present in the full view, fades as it collapses -->
|
||||
<svg
|
||||
class="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}"
|
||||
class="night-badge absolute -right-1 -bottom-0.5 rounded-full bg-card fill-foreground/60 p-px ring-1 ring-border/60"
|
||||
>
|
||||
<use
|
||||
xlink:href="/images/weather-icons/{getWeatherIconName(
|
||||
@@ -218,8 +197,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="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"
|
||||
class="detail-row flex w-full flex-col items-center gap-0.5 overflow-hidden text-[10px] tabular-nums text-muted-foreground"
|
||||
>
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<svg class="fill-sky-500" width="13" height="13">
|
||||
@@ -242,7 +220,6 @@
|
||||
<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"
|
||||
style="width:{CELL_W}px;height:{cellH}px"
|
||||
onclick={onExtend}
|
||||
aria-label="Load the longer-range forecast"
|
||||
>
|
||||
@@ -264,9 +241,30 @@
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* The bar background/divider/shadow fade in only once the strip has collapsed
|
||||
into its compact form, so at the top of the page it reads as plain cards. */
|
||||
/* Everything below is derived from a single custom property `--p` (0 = full
|
||||
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 {
|
||||
/* 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(
|
||||
in oklab,
|
||||
var(--color-background) calc(var(--chrome) * 100%),
|
||||
@@ -277,12 +275,37 @@
|
||||
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));
|
||||
}
|
||||
|
||||
.strip-cell,
|
||||
.strip-side {
|
||||
width: var(--cell-w);
|
||||
height: var(--cell-h);
|
||||
/* size tracks scroll exactly (no transition); only the tap highlight eases */
|
||||
transition:
|
||||
border-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) {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user