visual updates
This commit is contained in:
@@ -145,7 +145,7 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Drizzli | Weather</title>
|
||||
<title>Drizz.li | Weather</title>
|
||||
<link rel="canonical" href="https://drizz.li/weather/week" />
|
||||
<meta name="description" content="7-day weather forecast with detailed hourly data" />
|
||||
</svelte:head>
|
||||
|
||||
@@ -57,8 +57,8 @@
|
||||
// defer to after layout so the measured positions and scroll width are final
|
||||
requestAnimationFrame(() => {
|
||||
// scroll so the first day card sits exactly at the content edge (aligned
|
||||
// with the page hero), leaving the "past days" button off to the left
|
||||
el.scrollLeft += wrap.getBoundingClientRect().left - el.getBoundingClientRect().left - 12;
|
||||
// with the page hero), leaving the "past days" button fully off to the left
|
||||
el.scrollLeft += wrap.getBoundingClientRect().left - el.getBoundingClientRect().left;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -119,13 +119,17 @@
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<div transition:fade={{ duration: 200 }} class="mb-1 min-h-47.5 md:mb-6 md:min-h-65">
|
||||
<!-- negative margin + matching padding: the scroll box gains room so a
|
||||
lifted/scaled/shadowed card is never clipped, while the first card still
|
||||
lines up with the page content edge -->
|
||||
<div
|
||||
transition:fade={{ duration: 200 }}
|
||||
class="-mx-3 mb-1 flex min-h-47.5 items-stretch gap-2 px-3 md:mb-6 md:min-h-65"
|
||||
>
|
||||
<!-- A horizontally-scrolling rail (past button + day cards) plus a "load more"
|
||||
card pinned to the right so it's always visible on every screen. The
|
||||
matching top/bottom padding gives a lifted/scaled card room so it's never
|
||||
clipped, while the first card still lines up with the page content edge. -->
|
||||
<div
|
||||
bind:this={scrollEl}
|
||||
class="day-scroll -mx-3 flex gap-2 overflow-x-auto px-3 pt-2 pb-3 md:pt-5 md:pb-11"
|
||||
class="day-scroll flex min-w-0 flex-1 gap-2 overflow-x-auto pt-3 pb-3 md:pt-5 md:pb-11"
|
||||
class:scrolling
|
||||
onscroll={onScroll}
|
||||
>
|
||||
@@ -133,7 +137,7 @@
|
||||
{#if canExtendPast && onExtendPast}
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-24 shrink-0 cursor-pointer flex-col items-center justify-center gap-2 rounded-2xl border border-dashed border-border/70 bg-card/40 px-2 text-muted-foreground transition-colors hover:border-primary/50 hover:bg-primary/5 hover:text-foreground"
|
||||
class="mr-4 flex w-24 shrink-0 cursor-pointer flex-col items-center justify-center gap-2 rounded-2xl border border-dashed border-border/70 bg-card/40 px-2 text-muted-foreground transition-colors hover:border-primary/50 hover:bg-primary/5 hover:text-foreground"
|
||||
onclick={onExtendPast}
|
||||
aria-label="Load recent past days"
|
||||
>
|
||||
@@ -157,14 +161,9 @@
|
||||
</button>
|
||||
{/if}
|
||||
<!-- the cards fill at least the viewport so the row overflows past the
|
||||
"past days" button (letting it scroll out of view even on wide
|
||||
screens). On md+ we also reserve room so the "load more" button stays
|
||||
visible; on mobile it's simply reached by scrolling (never clipped). -->
|
||||
<div
|
||||
bind:this={cardsWrapEl}
|
||||
class="cards-fill flex gap-2"
|
||||
style="--fill-reserve: {canExtend ? '6.5rem' : '0rem'}"
|
||||
>
|
||||
"past days" button, letting it scroll out of view even on wide
|
||||
screens -->
|
||||
<div bind:this={cardsWrapEl} class="cards-fill flex gap-2">
|
||||
{#each daily.dailyDates as time, index (index)}
|
||||
{@const selected = isSameDayInZone(time, selectedDay, daily.timezone)}
|
||||
{@const tempMax = daily.daily.temperature_2m_max[index]}
|
||||
@@ -324,36 +323,67 @@
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if canExtend && onExtend}
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-24 shrink-0 cursor-pointer flex-col items-center justify-center gap-2 rounded-2xl border border-dashed border-border/70 bg-card/40 px-2 text-muted-foreground transition-colors hover:border-primary/50 hover:bg-primary/5 hover:text-foreground"
|
||||
onclick={onExtend}
|
||||
aria-label="Load the longer-range forecast"
|
||||
>
|
||||
<svg
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.75"
|
||||
<!-- Mobile: the "load more" card is the last item in the SAME flex row
|
||||
as the cards (reached by scrolling); keeping it in this container
|
||||
avoids the zoom/flex mis-position seen when it was a sibling. On
|
||||
md+ it's hidden here and the pinned sibling below is shown. -->
|
||||
{#if canExtend && onExtend}
|
||||
<button
|
||||
type="button"
|
||||
class="day-card-btn flex w-24 shrink-0 cursor-pointer flex-col items-center justify-center gap-2 self-stretch rounded-2xl border border-dashed border-border/70 bg-card/40 px-2 text-muted-foreground transition-colors hover:border-primary/50 hover:bg-primary/5 hover:text-foreground md:hidden"
|
||||
onclick={onExtend}
|
||||
aria-label="Load the longer-range forecast"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M8 7V3m8 4V3M4 11h16M5 21h14a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2z"
|
||||
/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 14v4m-2-2h4" />
|
||||
</svg>
|
||||
<span class="text-center text-[11px] leading-tight font-semibold">
|
||||
Load<br />15 days
|
||||
</span>
|
||||
</button>
|
||||
{/if}
|
||||
<svg
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.75"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M8 7V3m8 4V3M4 11h16M5 21h14a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2z"
|
||||
/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 14v4m-2-2h4" />
|
||||
</svg>
|
||||
<span class="text-center text-[11px] leading-tight font-semibold">
|
||||
Load<br />15 days
|
||||
</span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Desktop: "load more" card pinned to the right of the scroll rail (a
|
||||
sibling, not inside the scroll) so it's always visible and full-height. -->
|
||||
{#if daily && canExtend && onExtend}
|
||||
<button
|
||||
type="button"
|
||||
class="mt-2 mb-3 hidden w-20 shrink-0 cursor-pointer flex-col items-center justify-center gap-2 self-stretch rounded-2xl border border-dashed border-border/70 bg-card/40 px-2 text-muted-foreground transition-colors hover:border-primary/50 hover:bg-primary/5 hover:text-foreground md:mt-5 md:mb-11 md:flex md:w-24"
|
||||
onclick={onExtend}
|
||||
aria-label="Load the longer-range forecast"
|
||||
>
|
||||
<svg
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.75"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M8 7V3m8 4V3M4 11h16M5 21h14a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2z"
|
||||
/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 14v4m-2-2h4" />
|
||||
</svg>
|
||||
<span class="text-center text-[11px] leading-tight font-semibold"> Load<br />15 days </span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@@ -362,6 +392,10 @@
|
||||
.day-scroll {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: transparent transparent;
|
||||
/* Soft fade at the right edge (only) so cards dissolve into the page
|
||||
margin instead of being hard-cut as they scroll off. */
|
||||
-webkit-mask-image: linear-gradient(to right, #000 calc(100% - 32px), transparent);
|
||||
mask-image: linear-gradient(to right, #000 calc(100% - 32px), transparent);
|
||||
}
|
||||
.day-scroll.scrolling {
|
||||
scrollbar-color: color-mix(in oklab, var(--color-border) 85%, transparent) transparent;
|
||||
|
||||
@@ -281,6 +281,34 @@
|
||||
: null
|
||||
);
|
||||
|
||||
// ─── Start the scroll at 06:00 on day change (mobile) ───────────────────────
|
||||
// When the table overflows sideways, scroll so the 06:00 column is the leftmost
|
||||
// visible one — the early hours are rarely of interest and this keeps the
|
||||
// daytime in view on every day switch.
|
||||
let tableScrollEl = $state<HTMLDivElement>();
|
||||
let autoScrolledDay = -1;
|
||||
$effect(() => {
|
||||
const day = selectedDay.getTime(); // re-run on day switch
|
||||
const el = tableScrollEl;
|
||||
if (!el || cellData.length === 0 || tableWidth === 0 || headerColWidth === 0) return;
|
||||
if (autoScrolledDay === day) return;
|
||||
autoScrolledDay = day;
|
||||
requestAnimationFrame(() => {
|
||||
// only when it actually overflows (i.e. mobile / narrow)
|
||||
if (el.scrollWidth <= el.clientWidth + 4) {
|
||||
el.scrollLeft = 0;
|
||||
return;
|
||||
}
|
||||
const sixIdx = cellData.findIndex((c) => getZonedHour(c.date, data.timezone) >= 6);
|
||||
if (sixIdx <= 0) {
|
||||
el.scrollLeft = 0;
|
||||
return;
|
||||
}
|
||||
const colWidth = (tableWidth - headerColWidth) / cellData.length;
|
||||
el.scrollLeft = Math.max(0, sixIdx * colWidth);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Chart-hover mirror ─────────────────────────────────────────────────────
|
||||
// When the shared meteogram is hovered, highlight the matching table column
|
||||
// (only if the hovered time falls on the day the table is currently showing).
|
||||
@@ -387,7 +415,7 @@
|
||||
|
||||
<!-- Below the min-width the table scrolls sideways instead of squeezing;
|
||||
1h needs far more room than 3h (24 vs 8 columns) -->
|
||||
<div class="overflow-x-auto">
|
||||
<div class="overflow-x-auto" bind:this={tableScrollEl}>
|
||||
<div
|
||||
class="relative {is3h ? 'min-w-[560px]' : 'min-w-[1100px]'}"
|
||||
bind:clientWidth={tableWidth}
|
||||
|
||||
@@ -35,7 +35,9 @@
|
||||
if (liveCharts.length === 0 || downloadingPng) return;
|
||||
downloadingPng = true;
|
||||
try {
|
||||
await downloadChartsPng(liveCharts, 'week-forecast');
|
||||
// pair each chart with its panel title so the export is labelled
|
||||
const items = renderPanels.map((p, i) => ({ chart: chartComponents[i], title: p.title }));
|
||||
await downloadChartsPng(items, 'week-forecast');
|
||||
} finally {
|
||||
setTimeout(() => (downloadingPng = false), 500);
|
||||
}
|
||||
@@ -276,26 +278,25 @@
|
||||
<div
|
||||
class="rounded-xl border border-dashed border-border px-4 py-10 text-center text-sm text-muted-foreground"
|
||||
>
|
||||
No meteograms configured — <button
|
||||
No meteograms configured, <button
|
||||
class="cursor-pointer font-semibold text-primary underline-offset-2 hover:underline"
|
||||
onclick={() => (customizerOpen = true)}>add some variables</button
|
||||
>.
|
||||
</div>
|
||||
{:else}
|
||||
<!-- one full-bleed card on mobile / contained card on md+, graphs stacked
|
||||
<!-- one full-bleed card until lg / contained card on lg+, graphs stacked
|
||||
tightly so they read as one fluent meteogram -->
|
||||
<div class="-mx-3 border-y border-border/70 bg-card shadow-sm md:mx-0 md:rounded-2xl md:border">
|
||||
<div class="-mx-3 border-y border-border/70 bg-card shadow-sm lg:mx-0 lg:rounded-2xl lg:border">
|
||||
{#each renderPanels as panel, i (panel.id)}
|
||||
<div
|
||||
class="px-0 pt-2 pb-1 md:px-4 {i > 0 ? 'border-t border-border/50' : 'md:pt-4'} {i ===
|
||||
renderPanels.length - 1
|
||||
? 'pb-3 md:pb-4'
|
||||
: ''}"
|
||||
class="px-0 pt-0.5 pb-0 lg:px-4 lg:pt-2 lg:pb-1 {i > 0
|
||||
? 'border-t border-border/50'
|
||||
: 'lg:pt-4'} {i === renderPanels.length - 1 ? 'pb-1 lg:pb-4' : ''}"
|
||||
>
|
||||
<div class="mb-0.5 flex items-center justify-between px-3 md:px-0">
|
||||
<div class="mb-0 flex items-center justify-between px-3 lg:mb-0.5 lg:px-0">
|
||||
<h4 class="truncate text-xs font-bold tracking-wide text-muted-foreground uppercase">
|
||||
<span class="hidden md:inline">{panel.title}</span>
|
||||
<span class="md:hidden">{panel.titleShort}</span>
|
||||
<span class="hidden lg:inline">{panel.title}</span>
|
||||
<span class="lg:hidden">{panel.titleShort}</span>
|
||||
</h4>
|
||||
</div>
|
||||
<ChartContainer
|
||||
|
||||
Reference in New Issue
Block a user