try strip desktop
This commit is contained in:
@@ -30,10 +30,9 @@
|
||||
onExtendPast
|
||||
}: Props = $props();
|
||||
|
||||
// ─── Scroll-driven collapse (full → compact) ────────────────────────────────
|
||||
// The strip collapses from full cards to a compact strip as it sticks. All
|
||||
// sizing derives from a single registered custom property `--strip-p` (0 =
|
||||
// full, 1 = compact):
|
||||
// ─── Scroll-driven collapse (full → compact, mobile only) ───────────────────
|
||||
// All sizing derives from a single registered custom property `--strip-p`
|
||||
// (0 = full cards, 1 = compact strip):
|
||||
//
|
||||
// * Browsers with CSS scroll-driven animations (Chrome/Edge 115+, Safari 26+)
|
||||
// scrub `--strip-p` natively from a view-timeline on the sentinel below —
|
||||
@@ -41,6 +40,9 @@
|
||||
// * Everything else (Firefox, older Safari) snaps between the two states
|
||||
// with a short CSS transition instead: an IntersectionObserver on the same
|
||||
// sentinel toggles `.compact`. No per-frame scroll handler anywhere.
|
||||
//
|
||||
// On md+ the strip is pinned to the compact state (no animation) and docks
|
||||
// under the topbar as a slim always-sticky day picker.
|
||||
let sentinelEl = $state<HTMLDivElement>();
|
||||
let stripScrollEl = $state<HTMLDivElement>();
|
||||
let daysWrapEl = $state<HTMLDivElement>();
|
||||
@@ -83,19 +85,19 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- progress sentinel: an invisible 120px band (taking no layout space) just
|
||||
above the sticky strip. Its exit across the scrollport top drives the
|
||||
collapse — via view-timeline where supported, IntersectionObserver
|
||||
otherwise. It sits above the strip so the strip shrinking (which reflows
|
||||
the table below) never feeds back into the measurement. -->
|
||||
<!-- progress sentinel: an invisible band (taking no layout space) just above
|
||||
the sticky strip. Its exit across the scrollport top drives the collapse —
|
||||
via view-timeline where supported, IntersectionObserver otherwise. It sits
|
||||
above the strip so the strip shrinking never feeds back into the
|
||||
measurement. -->
|
||||
<div bind:this={sentinelEl} class="sentinel" aria-hidden="true"></div>
|
||||
|
||||
<div
|
||||
class="daystrip sticky -top-3 z-30 -mx-3 md:hidden"
|
||||
class="daystrip sticky -top-3 z-30 -mx-3 lg:-top-6 lg:-mx-8"
|
||||
class:js-snap={needsSnapFallback}
|
||||
class:compact
|
||||
>
|
||||
<div class="strip-row flex overflow-x-auto px-3 py-2" bind:this={stripScrollEl}>
|
||||
<div class="strip-row flex overflow-x-auto px-3 py-2 md:py-1 lg:px-8" bind:this={stripScrollEl}>
|
||||
{#if daily}
|
||||
{#if canExtendPast && onExtendPast}
|
||||
<button
|
||||
@@ -105,7 +107,7 @@
|
||||
aria-label="Load recent past days"
|
||||
>
|
||||
<svg
|
||||
class="h-5 w-5"
|
||||
class="h-5 w-5 md:h-4 md:w-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -133,21 +135,26 @@
|
||||
{#if tempMax != null && !isNaN(tempMax) && !(tempMax === 0 && tempMin === 0)}
|
||||
<button
|
||||
type="button"
|
||||
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 md:gap-0 md:pb-0 {selected
|
||||
? 'border-primary bg-primary/10 ring-1 ring-primary/50'
|
||||
: 'border-border/60 bg-card'}"
|
||||
aria-pressed={selected}
|
||||
onclick={() => onSelectDay(time, index)}
|
||||
>
|
||||
<!-- weekday drifts to the left edge and the date fades in at the
|
||||
right edge as the cards collapse (flex spacers driven by --rel) -->
|
||||
<span
|
||||
class="text-[11px] font-semibold tracking-wide whitespace-nowrap {selected
|
||||
class="dow-row flex w-full items-baseline px-0.5 text-[11px] font-semibold tracking-wide whitespace-nowrap md:text-[10px] {selected
|
||||
? 'text-primary'
|
||||
: ''}"
|
||||
>
|
||||
{formatZoned(time, daily.timezone, 'EEE').toUpperCase()}<span
|
||||
class="date-inline align-baseline font-medium tabular-nums text-muted-foreground"
|
||||
> {formatZoned(time, daily.timezone, 'd')}</span
|
||||
<span class="dow-spacer"></span>
|
||||
<span>{formatZoned(time, daily.timezone, 'EEE').toUpperCase()}</span>
|
||||
<span class="dow-mid"></span>
|
||||
<span class="date-inline text-right font-medium tabular-nums text-muted-foreground"
|
||||
>{formatZoned(time, daily.timezone, 'd')}</span
|
||||
>
|
||||
<span class="dow-spacer"></span>
|
||||
</span>
|
||||
|
||||
<span
|
||||
@@ -156,7 +163,7 @@
|
||||
{getRelativeDayLabel(time, daily.timezone)}
|
||||
</span>
|
||||
|
||||
<div class="relative">
|
||||
<div class="icon-wrap relative">
|
||||
<svg class="day-icon fill-foreground">
|
||||
<use
|
||||
xlink:href="/images/weather-icons/{getWeatherIconName(
|
||||
@@ -180,7 +187,7 @@
|
||||
|
||||
<div class="flex items-baseline gap-1 leading-none">
|
||||
<span
|
||||
class="temp-max rounded-md py-0.5 font-extrabold tabular-nums"
|
||||
class="temp-max rounded-md py-0.5 font-extrabold tabular-nums md:py-px"
|
||||
style="background-color:{maxStyle.bg};color:{maxStyle.fg}"
|
||||
>
|
||||
{tempMax.toFixed(0)}°
|
||||
@@ -218,7 +225,7 @@
|
||||
aria-label="Load the longer-range forecast"
|
||||
>
|
||||
<svg
|
||||
class="h-5 w-5"
|
||||
class="h-5 w-5 md:h-4 md:w-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -246,27 +253,32 @@
|
||||
initial-value: 0;
|
||||
}
|
||||
|
||||
/* Invisible 120px collapse band: the distance over which the collapse plays
|
||||
out. The negative margin removes it from layout so nothing shifts. */
|
||||
.sentinel {
|
||||
height: 120px;
|
||||
margin-bottom: -120px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Tunables, shared by the strip and its sentinel: the collapse plays out
|
||||
over exactly the cell-height difference (see .daystrip height below). */
|
||||
.sentinel,
|
||||
.daystrip {
|
||||
/* tunables */
|
||||
--cell-w-full: 76px;
|
||||
--cell-w-min: 56px;
|
||||
--cell-h-full: 146px;
|
||||
--cell-h-min: 66px;
|
||||
--cell-h-full: 140px;
|
||||
--cell-h-min: 62px;
|
||||
--icon-full: 44px;
|
||||
--icon-min: 21px;
|
||||
--pt-full: 7px;
|
||||
--pt-min: 2px;
|
||||
--gap-full: 8px;
|
||||
--gap-min: 4px;
|
||||
--collapse: calc(var(--cell-h-full) - var(--cell-h-min));
|
||||
}
|
||||
|
||||
/* Invisible collapse band: the scroll distance over which the collapse
|
||||
plays. The negative margin removes it from layout so nothing shifts. */
|
||||
.sentinel {
|
||||
height: var(--collapse);
|
||||
margin-bottom: calc(-1 * var(--collapse));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.daystrip {
|
||||
--strip-p: 0;
|
||||
|
||||
/* progress-derived (--k is the "fullness": 1 when full, 0 when compact) */
|
||||
@@ -278,12 +290,29 @@
|
||||
--gap: calc(var(--gap-min) + (var(--gap-full) - var(--gap-min)) * var(--k));
|
||||
--rel: clamp(0, calc(1 - var(--strip-p) * 2), 1);
|
||||
--detail: clamp(0, calc(1 - var(--strip-p) * 1.6), 1);
|
||||
/* bar background/divider fade in once it starts collapsing */
|
||||
--chrome: clamp(0, calc(var(--strip-p) / 0.35), 1);
|
||||
/* bar background turns opaque almost as soon as the strip sticks, so
|
||||
content never shows through it */
|
||||
--chrome: clamp(0, calc(var(--strip-p) * 6), 1);
|
||||
|
||||
/* Opaque-background fade only — no backdrop-filter blur or animated
|
||||
box-shadow (both are very expensive to repaint every scroll frame on
|
||||
mobile). Promote to its own compositor layer so repaints stay isolated. */
|
||||
/* The sticky box keeps a CONSTANT height — only its contents shrink.
|
||||
The collapse therefore never resizes the document (the large table
|
||||
below would otherwise reflow on every scroll frame: the main source
|
||||
of scroll jank on mobile Chromium), and because the collapse distance
|
||||
equals the height difference, the table slides up under the shrinking
|
||||
bar in exact sync. The empty lower part is click-through. */
|
||||
height: calc(var(--cell-h-full) + 16px);
|
||||
pointer-events: none;
|
||||
contain: layout style;
|
||||
/* own compositor layer: per-frame repaints stay isolated to the strip */
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
/* The visible bar: hugs the (shrinking) content and carries the chrome.
|
||||
No backdrop-filter blur or animated box-shadow — both are very expensive
|
||||
to repaint every scroll frame on mobile. */
|
||||
.strip-row {
|
||||
pointer-events: auto;
|
||||
gap: var(--gap);
|
||||
background: color-mix(
|
||||
in oklab,
|
||||
var(--color-background) calc(var(--chrome) * 100%),
|
||||
@@ -291,7 +320,6 @@
|
||||
);
|
||||
border-bottom: 1px solid
|
||||
color-mix(in oklab, var(--color-border) calc(var(--chrome) * 100%), transparent);
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
/* Scrub path: the sentinel's exit across the scrollport top maps directly to
|
||||
@@ -327,13 +355,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
.strip-row,
|
||||
/* md+: no collapse — the strip is a slim, always-compact day picker that
|
||||
docks under the topbar, matching its h-14 (56px) height exactly. */
|
||||
@media (min-width: 768px) {
|
||||
.sentinel,
|
||||
.daystrip {
|
||||
--cell-h-min: 48px;
|
||||
--icon-min: 20px;
|
||||
--gap-min: 6px;
|
||||
}
|
||||
.daystrip {
|
||||
--strip-p: 1;
|
||||
animation: none;
|
||||
height: 56px;
|
||||
}
|
||||
}
|
||||
|
||||
.strip-days {
|
||||
gap: var(--gap);
|
||||
}
|
||||
.strip-cell,
|
||||
.strip-side {
|
||||
width: var(--cell-w);
|
||||
height: var(--cell-h);
|
||||
/* size tracks the collapse exactly; only the tap highlight eases */
|
||||
transition:
|
||||
@@ -341,9 +383,22 @@
|
||||
background-color 0.15s;
|
||||
}
|
||||
.strip-cell {
|
||||
width: var(--cell-w);
|
||||
padding-top: var(--pt);
|
||||
}
|
||||
/* Side buttons keep the compact width in BOTH states, so almost nothing to
|
||||
the left of the first day changes size during the collapse — the first
|
||||
day stays put instead of drifting off-screen. */
|
||||
.strip-side {
|
||||
width: var(--cell-w-min);
|
||||
}
|
||||
.icon-wrap {
|
||||
/* tuck the icon into its line box: the glyphs carry generous built-in
|
||||
padding, so pulling the neighbours in keeps the cells tight */
|
||||
margin-block: -2px -3px;
|
||||
}
|
||||
.day-icon {
|
||||
display: block;
|
||||
width: var(--icon);
|
||||
height: var(--icon);
|
||||
}
|
||||
@@ -352,17 +407,17 @@
|
||||
height: calc(var(--icon) * 0.44);
|
||||
opacity: var(--rel);
|
||||
}
|
||||
/* Day-of-month slides in next to the weekday as the cards collapse
|
||||
("MON" → "MON 12"), replacing the relative label that fades out. */
|
||||
/* weekday centered when full, pushed to the edges when compact */
|
||||
.dow-spacer {
|
||||
flex-grow: var(--rel);
|
||||
}
|
||||
.dow-mid {
|
||||
flex-grow: calc(1 - var(--rel));
|
||||
}
|
||||
.date-inline {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
/* overflow≠visible makes an inline-block's baseline its bottom edge, which
|
||||
would render the digits superscript; text-bottom restores baseline
|
||||
alignment (same font size as the weekday, so descents match). */
|
||||
vertical-align: text-bottom;
|
||||
max-width: calc(20px * (1 - var(--rel)));
|
||||
/* width 0 while the cards are full so the weekday stays exactly centered */
|
||||
width: calc(14px * (1 - var(--rel)));
|
||||
opacity: calc(1 - var(--rel));
|
||||
}
|
||||
.rel-label {
|
||||
|
||||
Reference in New Issue
Block a user