2 Commits
13 changed files with 343 additions and 129 deletions
@@ -1,7 +1,12 @@
<script lang="ts">
import { createEventDispatcher, onDestroy, tick } from 'svelte';
import { type GeoLocation } from '$lib/stores/settings';
import {
type GeoLocation,
locationKey,
storedFavoriteLocations,
storedRecentLocations
} from '$lib/stores/settings';
import * as Alert from '$lib/components/ui/alert';
import { Button } from '$lib/components/ui/button';
@@ -30,11 +35,31 @@
};
const selectLocation = (location: GeoLocation) => {
addRecent(location);
searchQuery = '';
closePopover();
dispatch('location', location);
};
function addRecent(loc: GeoLocation) {
const key = locationKey(loc);
storedRecentLocations.update((list) =>
[loc, ...list.filter((l) => locationKey(l) !== key)].slice(0, 8)
);
}
function toggleFavorite(loc: GeoLocation) {
const key = locationKey(loc);
storedFavoriteLocations.update((list) =>
list.some((l) => locationKey(l) === key)
? list.filter((l) => locationKey(l) !== key)
: [loc, ...list].slice(0, 24)
);
}
$: favKeys = new Set($storedFavoriteLocations.map(locationKey));
$: recentToShow = $storedRecentLocations.filter((l) => !favKeys.has(locationKey(l)));
async function focusInput() {
await tick();
searchInputEl?.focus();
@@ -101,6 +126,55 @@
})();
</script>
{#snippet locationRow(location: GeoLocation)}
{@const fav = favKeys.has(locationKey(location))}
<div
class="group flex items-center rounded-md border border-transparent transition-[background,border-color] duration-150 hover:border-border hover:bg-accent"
>
<button
class="flex min-w-0 flex-1 cursor-pointer items-center gap-2.5 rounded-md px-2.5 py-2 text-left"
onclick={() => selectLocation(location)}
>
<img
class="h-7 w-7 shrink-0 rounded-full"
src="/images/country-flags/{(location.country_code || 'united_nations').toLowerCase()}.svg"
alt={location.country}
/>
<div class="min-w-0 flex-1">
<div class="truncate text-sm font-medium text-foreground">{location.name}</div>
<div class="truncate text-xs text-muted-foreground">
{location.admin1 || ''}
{location.country || ''}
· {location.latitude.toFixed(2)}°N {location.longitude.toFixed(2)}°E
{#if location.elevation}· {location.elevation.toFixed(0)}m{/if}
</div>
</div>
</button>
<button
class="mr-1 flex h-7 w-7 shrink-0 cursor-pointer items-center justify-center rounded-md hover:bg-background hover:text-amber-500 {fav
? 'text-amber-500'
: 'text-muted-foreground/50'}"
onclick={() => toggleFavorite(location)}
aria-label={fav ? 'Remove from favorites' : 'Add to favorites'}
title={fav ? 'Remove from favorites' : 'Add to favorites'}
>
<svg
class="h-4 w-4"
viewBox="0 0 24 24"
fill={fav ? 'currentColor' : 'none'}
stroke="currentColor"
stroke-width="1.75"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 3.6l2.5 5.1 5.6.8-4 3.9 1 5.6-5.1-2.7-5 2.7 1-5.6-4-3.9 5.5-.8z"
/>
</svg>
</button>
</div>
{/snippet}
<Popover.Root bind:open={popoverOpen}>
<Popover.Trigger
class="flex h-10 w-full cursor-pointer items-center gap-2.5 rounded-full border-2 border-primary/30 bg-background px-4 text-[0.8125rem] font-medium text-muted-foreground shadow-xs transition-[border-color,box-shadow] duration-150 hover:border-primary/70 hover:shadow-md"
@@ -178,8 +252,35 @@
</div>
</div>
{:then results}
{#if results.results && results.results.length === 0}
{#if searchQuery.length < 2}
{#if $storedFavoriteLocations.length > 0 || recentToShow.length > 0}
{#if $storedFavoriteLocations.length > 0}
<div
class="mb-1 px-1 text-[11px] font-semibold tracking-wide text-muted-foreground uppercase"
>
Favorites
</div>
<div class="space-y-0.5">
{#each $storedFavoriteLocations as loc (locationKey(loc))}
{@render locationRow(loc)}
{/each}
</div>
{/if}
{#if recentToShow.length > 0}
<div
class="mb-1 px-1 text-[11px] font-semibold tracking-wide text-muted-foreground uppercase {$storedFavoriteLocations.length
? 'mt-3'
: ''}"
>
Recent
</div>
<div class="space-y-0.5">
{#each recentToShow as loc (locationKey(loc))}
{@render locationRow(loc)}
{/each}
</div>
{/if}
{:else}
<div
class="flex items-start gap-2 rounded-md bg-primary/8 p-2.5 text-muted-foreground"
>
@@ -200,7 +301,14 @@
Start typing to search or use GPS to detect your position
</span>
</div>
{:else}
{/if}
{:else if results.results && results.results.length > 0}
<div class="space-y-0.5">
{#each results.results as location, i (i)}
{@render locationRow(location)}
{/each}
</div>
{:else if results.results}
<Alert.Root
class="border-orange-200 bg-orange-50 dark:border-orange-800 dark:bg-orange-900/20"
>
@@ -208,54 +316,10 @@
No locations found for "{searchQuery}". Try a different term.
</Alert.Description>
</Alert.Root>
{/if}
{:else if !results.results}
{:else}
<Alert.Root variant="destructive">
<Alert.Description>No locations found</Alert.Description>
</Alert.Root>
{:else}
<div class="space-y-0.5">
{#each results.results || [] as location, i (i)}
<button
class="group block w-full cursor-pointer rounded-md border border-transparent bg-transparent px-2.5 py-2 transition-[background,border-color] duration-150 hover:border-border hover:bg-accent"
onclick={() => selectLocation(location)}
>
<div class="flex items-center gap-2.5">
<img
class="h-7 w-7 rounded-full"
src="/images/country-flags/{(
location.country_code || 'united_nations'
).toLowerCase()}.svg"
alt={location.country}
/>
<div class="flex-1 text-left">
<div class="text-sm font-medium text-foreground">
{location.name}
</div>
<div class="text-xs text-muted-foreground">
{location.admin1 || ''}
{location.country || ''}
· {location.latitude.toFixed(2)}°N {location.longitude.toFixed(2)}°E
{#if location.elevation}· {location.elevation.toFixed(0)}m{/if}
</div>
</div>
<svg
class="h-3.5 w-3.5 text-muted-foreground opacity-0 transition-opacity duration-150 group-hover:opacity-100"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
/>
</svg>
</div>
</button>
{/each}
</div>
{/if}
{:catch error}
<Alert.Root variant="destructive">
+15 -6
View File
@@ -10,6 +10,7 @@
import { buildLocationRoute } from '$lib/utils/location';
import LocationSearch from '$lib/components/location/location-search.svelte';
import UnitSelector from '$lib/components/unit-selector.svelte';
interface Props {
onMenuToggle?: () => void;
@@ -68,17 +69,22 @@
<!-- Current location display -->
{#if location}
<div
class="hidden items-center gap-2 rounded-full border border-border/70 bg-muted/40 py-1 ps-1 pe-3 sm:flex"
class="hidden items-center gap-2 rounded-full border border-border/70 bg-muted/40 py-1 ps-1 pe-3 lg:flex"
>
<img
class="h-6 w-6 rounded-full ring-1 ring-border"
class="h-6 w-6 shrink-0 rounded-full ring-1 ring-border"
src="/images/country-flags/{(location.country_code || 'united_nations').toLowerCase()}.svg"
alt={location.country}
/>
<!-- the region lives here; the page hero carries the place name -->
<span class="text-sm font-semibold text-foreground">
{#if location.admin1}{location.admin1}, {location.country}{:else}{location.country ??
location.name}{/if}
<!-- full location (desktop); the page hero carries it on smaller screens -->
<span class="whitespace-nowrap text-sm font-semibold text-foreground">
{location.name}
{#if location.admin1 || location.country}
<span class="font-normal text-muted-foreground">
· {#if location.admin1}{location.admin1},&nbsp;
{/if}{location.country ?? ''}
</span>
{/if}
</span>
</div>
{/if}
@@ -96,6 +102,9 @@
/>
</div>
<!-- Measurement units -->
<UnitSelector />
<!-- Theme toggle: system → light → dark -->
<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"
@@ -125,7 +125,8 @@
{/each}
</nav>
<!-- Collapse toggle -->
<!-- Collapse toggle (desktop sidebar only; the mobile drawer omits onToggle) -->
{#if onToggle}
<div class="border-t border-sidebar-border px-2 py-3">
<button
class="relative flex w-full items-center rounded-md px-2.5 py-2 text-sm font-medium text-sidebar-foreground opacity-70 transition-colors duration-150 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:opacity-100"
@@ -141,7 +142,11 @@
viewBox="0 0 24 24"
stroke-width="1.75"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M11 19l-7-7 7-7m8 14l-7-7 7-7"
/>
</svg>
</div>
{#if !collapsed}
@@ -149,6 +154,7 @@
{/if}
</button>
</div>
{/if}
</aside>
<style>
@@ -44,8 +44,9 @@
<Popover.Root>
<Popover.Trigger
class="flex h-11 cursor-pointer items-center gap-2 rounded-xl border-2 border-border bg-card px-3.5 text-sm font-semibold text-muted-foreground shadow-sm transition-colors hover:border-primary/50 hover:text-foreground data-[state=open]:border-primary/60 data-[state=open]:text-foreground"
class="flex h-9 shrink-0 cursor-pointer items-center gap-1.5 rounded-full border border-border/70 px-2.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground data-[state=open]:bg-muted data-[state=open]:text-foreground"
aria-label="Choose measurement units"
title="Units"
>
<!-- gauge icon -->
<svg
@@ -55,7 +56,6 @@
viewBox="0 0 24 24"
stroke-width="1.75"
>
<path stroke-linecap="round" d="M12 13a9 9 0 0 1 8.5-9a9 9 0 0 1-8.5 15" opacity="0" />
<path
stroke-linecap="round"
stroke-linejoin="round"
@@ -63,7 +63,7 @@
/>
<circle cx="12" cy="15" r="1" fill="currentColor" stroke="none" />
</svg>
<span class="hidden md:inline">Units</span>
<span class="hidden text-xs font-semibold sm:inline">Units</span>
</Popover.Trigger>
<Popover.Content align="end" class="w-64 border-border">
<div class="flex flex-col gap-4">
+9
View File
@@ -114,3 +114,12 @@ export const defaultUnits: UnitPrefs = {
};
export const storedUnits = persisted<UnitPrefs>('units_v1', defaultUnits);
/** Recently visited and starred locations, shown in the search dropdown. */
export const storedRecentLocations = persisted<GeoLocation[]>('recent_locations_v1', []);
export const storedFavoriteLocations = persisted<GeoLocation[]>('favorite_locations_v1', []);
/** Stable key for de-duping locations (geocoding id, or rounded coordinates). */
export function locationKey(l: GeoLocation): string {
return l.id && l.id !== 0 ? `id:${l.id}` : `c:${l.latitude.toFixed(3)},${l.longitude.toFixed(3)}`;
}
+7 -1
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import { fade, fly } from 'svelte/transition';
import { page } from '$app/stores';
import { storedTheme } from '$lib/stores/settings';
@@ -63,10 +65,14 @@
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="absolute inset-0 bg-black/30"
transition:fade={{ duration: 150 }}
onclick={closeMobileMenu}
onkeydown={closeMobileMenu}
></div>
<div class="relative z-1 h-full w-55 shadow-lg">
<div
class="relative z-1 h-full w-55 shadow-lg"
transition:fly={{ x: -220, duration: 200, opacity: 1 }}
>
<WeatherNav collapsed={false} onMobileClose={closeMobileMenu} />
</div>
</div>
@@ -17,7 +17,6 @@
import { defaultParameters, ensembleModelGroups } from '../../options';
import ModelSelector from '../../week/[location]/ModelSelector.svelte';
import UnitSelector from '../../week/[location]/UnitSelector.svelte';
import type { PageData } from './$types';
@@ -155,6 +154,11 @@
fetchedData ? fetchedData.timestamps.slice(0, validLength).map((t) => t / 1000) : []
);
// Surface a note when the chosen model's ensemble stops short of the request.
let fullHours = $derived(fetchedData?.timestamps.length ?? 0);
let validDays = $derived(Math.max(0, Math.round(validLength / 24)));
let isTrimmed = $derived(!!fetchedData && validLength > 0 && validLength < fullHours - 1);
interface ChartDef {
title?: string;
subtitle?: string;
@@ -165,6 +169,23 @@
const BAND_COLOR = 'rgba(115, 192, 222, 0.9)';
// Human labels for the plotted ensemble variables (API names → readable title)
const VAR_LABELS: Record<string, string> = {
temperature_2m: 'Temperature',
apparent_temperature: 'Feels like',
precipitation: 'Precipitation',
rain: 'Rain',
snowfall: 'Snowfall',
wind_speed_10m: 'Wind speed',
wind_gusts_10m: 'Wind gusts',
relative_humidity_2m: 'Relative humidity',
cloud_cover: 'Cloud cover',
pressure_msl: 'Pressure (MSL)',
dew_point_2m: 'Dew point'
};
const varLabel = (v: string): string =>
VAR_LABELS[v] ?? v.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
let chartDefs = $derived.by((): ChartDef[] => {
if (!fetchedData) return [];
@@ -217,10 +238,11 @@
const isLast = vi === variables.length - 1;
defs.push({
title: isFirst ? 'Ensemble Spread' : undefined,
// each chart is labelled so the variable is obvious at a glance
title: `${varLabel(variable)}${isColumn ? '' : ' spread'}`,
subtitle: isFirst
? `${variable} min/mean/max across ${memberCount} members (${params.models?.[0] ?? ''})`
: undefined,
? `min · mean · max across ${memberCount} ensemble members`
: `min · mean · max (${unit})`,
unit,
showCredit: isLast,
series
@@ -244,7 +266,12 @@
<h1 class="truncate text-2xl leading-tight font-bold tracking-tight md:text-3xl">
{location.name}
</h1>
<p class="truncate text-sm text-muted-foreground">14-day ensemble forecast</p>
<p class="truncate text-sm text-muted-foreground">
<span class="lg:hidden"
>{#if location.admin1}{location.admin1},
{/if}{location.country ?? ''}<span class="mx-1 opacity-50">·</span></span
>14-day ensemble forecast
</p>
</div>
</div>
@@ -258,12 +285,35 @@
storedEnsembleModel.set(model);
}}
/>
<UnitSelector />
</div>
</div>
<!-- ─── Chart Area ─────────────────────────────────────────────────────────── -->
{#if isTrimmed}
<div
class="mb-4 flex items-start gap-2 rounded-md border border-amber-300/60 bg-amber-50 px-3.5 py-2.5 text-sm text-amber-800 dark:border-amber-800/50 dark:bg-amber-950/30 dark:text-amber-200"
>
<svg
class="mt-0.5 h-4 w-4 shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 9v4m0 4h.01M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"
/>
</svg>
<span>
This model's ensemble only reaches about <strong>{validDays} days</strong> ahead — the spread
is trimmed to its available range.
</span>
</div>
{/if}
{#if loadError}
<div
class="mb-4 rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive"
@@ -27,7 +27,6 @@
import { findModel, hourly, modelGroups } from '../../options';
import { defaultParameters } from '../../options';
import UnitSelector from '../../week/[location]/UnitSelector.svelte';
import ModelPictogramTimeline from './ModelPictogramTimeline.svelte';
import type { PageData } from './$types';
@@ -277,13 +276,10 @@
<div class="mt-6 md:mt-10">
<ChartToolbar charts={liveCharts} fileName="model-comparison">
{#snippet controls()}
<div class="flex items-center gap-4">
<div class="flex items-center gap-2">
<Switch id="show_legend" name="Show legend" bind:checked={showLegend} />
<Label for="show_legend" class="cursor-pointer text-base leading-none">Show legend</Label>
</div>
<UnitSelector />
</div>
{/snippet}
</ChartToolbar>
</div>
+12 -9
View File
@@ -38,9 +38,9 @@ const weatherCodes: Record<number, string> = {
51: 'sprinkle',
52: 'rain',
53: 'rain',
54: 'snowflake-cold',
55: 'snowflake-cold',
56: 'snowflake-cold',
54: 'sprinkle',
55: 'rain',
56: 'rain-mix',
57: 'sprinkle',
58: 'rain',
60: 'sprinkle',
@@ -56,11 +56,11 @@ const weatherCodes: Record<number, string> = {
71: 'snow',
72: 'snow',
73: 'snow',
74: 'snowflake-cold',
75: 'snowflake-cold',
76: 'snowflake-cold',
74: 'snow',
75: 'snow',
76: 'snow',
77: 'snow',
78: 'snowflake-cold',
78: 'snow',
80: 'rain',
81: 'sprinkle',
82: 'rain',
@@ -80,10 +80,13 @@ const weatherCodes: Record<number, string> = {
99: 'tornado'
};
// These conditions ship only as a single neutral glyph (no day/night variant).
const NEUTRAL_ICONS = new Set(['snowflake-cold', 'strong-wind', 'dust', 'tornado']);
export function getWeatherIconName(code: number, daytime: boolean): string {
const prefix = daytime ? 'day' : 'night';
const name = weatherCodes[code as keyof typeof weatherCodes] ?? 'clear';
return `wi-${prefix}-${name}`;
if (NEUTRAL_ICONS.has(name)) return `wi-${name}`;
return `wi-${daytime ? 'day' : 'night'}-${name}`;
}
export default weatherCodes;
@@ -20,7 +20,6 @@
import HourlyTable from './HourlyTable.svelte';
import MeteogramCharts from './MeteogramCharts.svelte';
import ModelSelector from './ModelSelector.svelte';
import UnitSelector from './UnitSelector.svelte';
import VariableSidebar from './VariableSidebar.svelte';
import { neededHourlyApiVars } from './variables';
@@ -72,6 +71,8 @@
// 7 by default; the user can extend to the model's longer range (up to 16 days)
let forecastDays = $state(7);
// 0 by default; the user can pull in a few recent past days
let pastDays = $state(0);
const selectedDay = new SvelteDate();
@@ -111,7 +112,7 @@
wind_speed_unit: params.wind_speed_unit as 'kmh' | 'ms' | 'mph' | 'kn',
precipitation_unit: params.precipitation_unit as 'mm' | 'inch',
forecast_days: forecastDays,
past_days: 0,
past_days: pastDays,
timezone: loc.timezone
})
.then((result: WeekForecastResult) => {
@@ -164,7 +165,12 @@
<h1 class="truncate text-2xl leading-tight font-bold tracking-tight md:text-3xl">
{location.name}
</h1>
<p class="truncate text-sm text-muted-foreground">7-day forecast</p>
<p class="truncate text-sm text-muted-foreground">
<span class="lg:hidden"
>{#if location.admin1}{location.admin1},
{/if}{location.country ?? ''}<span class="mx-1 opacity-50">·</span></span
>7-day forecast
</p>
</div>
</div>
@@ -174,10 +180,11 @@
onModelChange={(model) => {
params.models = [model];
storedModel.set(model);
forecastDays = 7; // a new model may not support the extended range
// a new model may not support the extended / past range
forecastDays = 7;
pastDays = 0;
}}
/>
<UnitSelector />
</div>
</div>
@@ -198,6 +205,8 @@
onSelectDay={switchDay}
canExtend={forecastDays < 15}
onExtend={() => (forecastDays = 15)}
canExtendPast={pastDays < 3}
onExtendPast={() => (pastDays = 3)}
/>
{#if fetchedHourly && fetchedDaily}
@@ -15,9 +15,44 @@
/** Offer a button after the last day to load the model's longer range */
canExtend?: boolean;
onExtend?: () => void;
/** Offer a button before the first day to load recent past days */
canExtendPast?: boolean;
onExtendPast?: () => void;
}
let { daily, selectedDay, units, onSelectDay, canExtend = false, onExtend }: Props = $props();
let {
daily,
selectedDay,
units,
onSelectDay,
canExtend = false,
onExtend,
canExtendPast = false,
onExtendPast
}: Props = $props();
// The "past days" button sits before the first card but starts scrolled out
// of view — the user reveals it by scrolling left. Re-hide only when the
// dataset (location) changes, not on every re-render.
let scrollEl = $state<HTMLDivElement>();
let pastBtnEl = $state<HTMLButtonElement>();
let hiddenForRef: FetchedDaily | null = null;
$effect(() => {
const d = daily;
if (!d || !canExtendPast || !scrollEl || !pastBtnEl || hiddenForRef === d) return;
hiddenForRef = d;
const el = scrollEl;
const btn = pastBtnEl;
// defer to after layout so the measured positions and scroll width are final
requestAnimationFrame(() => {
const b = btn.getBoundingClientRect();
const c = el.getBoundingClientRect();
// scroll so the button's right edge sits just past the left edge (a small
// gap of extra margin keeps the first day card from hugging the edge)
el.scrollLeft += b.right - c.left + 6;
});
});
function getDaylightSeconds(index: number): number {
if (!daily) return 0;
@@ -77,10 +112,42 @@
</svg>
<div in:fade out:fade class="mb-6 min-h-[260px]">
<!-- generous padding on all sides so a lifted/scaled active or hovered card
(and its shadow) is never clipped by the horizontal scroll container -->
<div class="flex gap-2 overflow-x-auto px-3 pt-3 pb-5" style="scrollbar-width: thin">
<!-- 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
bind:this={scrollEl}
class="-mx-3 flex gap-2 overflow-x-auto px-3 pt-5 pb-11"
style="scrollbar-width: thin"
>
{#if daily}
{#if canExtendPast && onExtendPast}
<button
bind:this={pastBtnEl}
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={onExtendPast}
aria-label="Load recent past days"
>
<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="M14 13l-3 3 3 3" />
</svg>
<span class="text-center text-[11px] leading-tight font-semibold">
Past<br />3 days
</span>
</button>
{/if}
{#each daily.dailyDates as time, index (index)}
{@const selected = isSameDayInZone(time, selectedDay, daily.timezone)}
{@const tempMax = daily.daily.temperature_2m_max[index]}
@@ -264,15 +331,10 @@
</div>
<style>
/* Mobile: keep the exact desktop layout (so nothing wraps), then scale the
whole card down uniformly. A fixed width matches a full desktop card and
the row scrolls horizontally. */
/* Mobile: keep the exact desktop layout, just scale the whole card down. */
@media (max-width: 768px) {
.day-card {
flex: 0 0 auto;
width: 168px;
max-width: none;
zoom: 0.82;
zoom: 0.72;
}
}
</style>
@@ -387,7 +387,7 @@
<!-- "Now" label, aligned with the sunrise/sunset labels along the bottom -->
{#if isTodaySelected && nowPercent != null}
<span
class="absolute bottom-0.5 z-30 -translate-x-1/2 rounded-full bg-red-500 px-1.5 py-px text-[9px] font-bold tracking-wide whitespace-nowrap text-white uppercase shadow-sm"
class="absolute bottom-0.5 z-15 -translate-x-1/2 rounded-full bg-red-500 px-1.5 py-px text-[9px] font-bold tracking-wide whitespace-nowrap text-white uppercase shadow-sm"
style="left:{nowPercent}%"
>
Now
@@ -241,7 +241,7 @@
{#each renderPanels as panel, i (panel.id)}
<!-- full-bleed to the screen edges on mobile; a contained card on md+ -->
<div
class="-mx-5 border-y border-border/70 bg-card px-1 py-3 shadow-sm md:mx-0 md:rounded-2xl md:border md:px-4 md:py-4"
class="-mx-5 border-y border-border/70 bg-card px-0 py-3 shadow-sm md:mx-0 md:rounded-2xl md:border md:px-4 md:py-4"
>
<div class="mb-1 flex items-center justify-between px-3 md:px-1">
<h4 class="truncate text-sm font-bold text-muted-foreground">