darkmode
This commit is contained in:
@@ -1 +1,46 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||
<title>OMbrella</title>
|
||||
<defs>
|
||||
<linearGradient id="sky" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#e0f2fe" />
|
||||
<stop offset="1" stop-color="#bae6fd" />
|
||||
</linearGradient>
|
||||
<linearGradient id="canopy" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#fb923c" />
|
||||
<stop offset="1" stop-color="#ea580c" />
|
||||
</linearGradient>
|
||||
<linearGradient id="drop" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#38bdf8" />
|
||||
<stop offset="1" stop-color="#2563eb" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="64" height="64" rx="14" fill="url(#sky)" />
|
||||
<!-- raindrops falling onto the umbrella -->
|
||||
<path d="M12 5c2 2.9 3 4.6 3 6.2a3 3 0 0 1-6 0c0-1.6 1-3.3 3-6.2Z" fill="url(#drop)" />
|
||||
<path d="M53 4c2 2.9 3 4.6 3 6.2a3 3 0 0 1-6 0c0-1.6 1-3.3 3-6.2Z" fill="url(#drop)" />
|
||||
<path d="M23 2.5c1.7 2.4 2.5 3.9 2.5 5.2a2.5 2.5 0 0 1-5 0c0-1.3.8-2.8 2.5-5.2Z" fill="url(#drop)" />
|
||||
<path d="M45 12c1.7 2.4 2.5 3.9 2.5 5.2a2.5 2.5 0 0 1-5 0c0-1.3.8-2.8 2.5-5.2Z" fill="url(#drop)" />
|
||||
<!-- pole with curved handle -->
|
||||
<path
|
||||
d="M33 35v17a4.5 4.5 0 0 1-9 0"
|
||||
fill="none"
|
||||
stroke="#475569"
|
||||
stroke-width="3.25"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<!-- canopy tip -->
|
||||
<path d="M33 13.5v4" fill="none" stroke="#475569" stroke-width="3" stroke-linecap="round" />
|
||||
<!-- canopy with scalloped edge -->
|
||||
<path
|
||||
d="M11 37c0-11.6 9.8-21 22-21s22 9.4 22 21c-1.8-2.9-5.5-2.9-7.3 0-1.9-2.9-5.5-2.9-7.4 0-1.8-2.9-5.5-2.9-7.3 0-1.9-2.9-5.5-2.9-7.4 0-1.8-2.9-5.5-2.9-7.3 0-1.9-2.9-5.5-2.9-7.3 0Z"
|
||||
fill="url(#canopy)"
|
||||
/>
|
||||
<!-- ribs -->
|
||||
<path
|
||||
d="M33 16.5c-5.2 3-7.4 11-7.3 19M33 16.5c5.2 3 7.4 11 7.3 19"
|
||||
fill="none"
|
||||
stroke="#9a3412"
|
||||
stroke-width="1.5"
|
||||
opacity="0.35"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.8 KiB |
@@ -4,7 +4,7 @@
|
||||
Renders line and bar series over a shared hourly time axis on a
|
||||
devicePixelRatio-aware canvas. Supports daylight background bands,
|
||||
timezone-aware axis labels with emphasized day boundaries, a DOM tooltip
|
||||
with crosshair, wheel/pinch zoom, drag panning, and cross-chart
|
||||
with crosshair, Ctrl+wheel / pinch zoom, drag panning, and cross-chart
|
||||
synchronization via the `group` prop.
|
||||
|
||||
Usage:
|
||||
@@ -161,6 +161,16 @@
|
||||
let themeVersion = $state(0);
|
||||
const legendHidden = new SvelteSet<string>();
|
||||
|
||||
// Briefly shown when the user scrolls over the chart without holding Ctrl
|
||||
let zoomHintVisible = $state(false);
|
||||
let zoomHintTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
function showZoomHint(): void {
|
||||
zoomHintVisible = true;
|
||||
clearTimeout(zoomHintTimeout);
|
||||
zoomHintTimeout = setTimeout(() => (zoomHintVisible = false), 1200);
|
||||
}
|
||||
|
||||
// Zoom range and crosshair: either group-shared or local to this chart.
|
||||
// The group is acquired once at component init (the prop is treated as fixed).
|
||||
const groupName = untrack(() => group);
|
||||
@@ -170,6 +180,7 @@
|
||||
|
||||
onDestroy(() => {
|
||||
if (groupName) releaseGroup(groupName);
|
||||
clearTimeout(zoomHintTimeout);
|
||||
});
|
||||
|
||||
// ─── Derived: view window & scales ──────────────────────────────────────────
|
||||
@@ -737,6 +748,12 @@
|
||||
};
|
||||
|
||||
const onWheel = (e: WheelEvent): void => {
|
||||
// Zoom only while Ctrl (or ⌘) is held — a plain scroll should keep
|
||||
// scrolling the page. Trackpad pinch also arrives as ctrlKey wheel.
|
||||
if (!e.ctrlKey && !e.metaKey) {
|
||||
showZoomHint();
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
const t = pixToTime(localX(e));
|
||||
zoomAt(t, e.deltaY < 0 ? 1 / 1.3 : 1.3);
|
||||
@@ -803,6 +820,19 @@
|
||||
style:touch-action="pan-y"
|
||||
></canvas>
|
||||
|
||||
{#if zoomHintVisible}
|
||||
<div
|
||||
class="pointer-events-none absolute inset-0 z-30 flex items-center justify-center rounded-md bg-black/25 transition-opacity"
|
||||
>
|
||||
<span
|
||||
class="rounded-lg bg-popover px-4 py-2 text-sm font-medium text-popover-foreground shadow-lg"
|
||||
>
|
||||
Hold <kbd class="rounded border border-border bg-muted px-1.5 py-0.5 text-xs">Ctrl</kbd>
|
||||
and scroll to zoom
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if tooltipVisible}
|
||||
<div
|
||||
class="pointer-events-none absolute z-20 rounded-md border border-border bg-popover px-3 py-2 text-xs whitespace-nowrap text-popover-foreground shadow-md"
|
||||
|
||||
@@ -103,10 +103,10 @@
|
||||
|
||||
<Popover.Root bind:open={popoverOpen}>
|
||||
<Popover.Trigger
|
||||
class="flex h-9 w-full cursor-pointer items-center gap-2 rounded-md border border-border bg-background px-3 text-[0.8125rem] text-muted-foreground transition-[border-color,box-shadow] duration-150 hover:border-primary"
|
||||
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"
|
||||
>
|
||||
<svg
|
||||
class="h-3.5 w-3.5 shrink-0 opacity-50"
|
||||
class="h-4 w-4 shrink-0 text-primary"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
import { type GeoLocation, storedLocation } from '$lib/stores/settings';
|
||||
import { type GeoLocation, type Theme, storedLocation, storedTheme } from '$lib/stores/settings';
|
||||
|
||||
import { buildLocationRoute } from '$lib/utils/location';
|
||||
|
||||
@@ -23,6 +23,19 @@
|
||||
location = value;
|
||||
});
|
||||
|
||||
const themeCycle: Theme[] = ['system', 'light', 'dark'];
|
||||
const themeTitles: Record<Theme, string> = {
|
||||
system: 'Theme: follow system',
|
||||
light: 'Theme: light',
|
||||
dark: 'Theme: dark'
|
||||
};
|
||||
|
||||
function cycleTheme() {
|
||||
storedTheme.update(
|
||||
(current) => themeCycle[(themeCycle.indexOf(current) + 1) % themeCycle.length]
|
||||
);
|
||||
}
|
||||
|
||||
function navigateToLocation(newLocation: GeoLocation) {
|
||||
storedLocation.set(newLocation);
|
||||
const locationRoute = buildLocationRoute(newLocation);
|
||||
@@ -54,17 +67,19 @@
|
||||
|
||||
<!-- Current location display -->
|
||||
{#if location}
|
||||
<div class="hidden items-center gap-2 sm:flex">
|
||||
<div
|
||||
class="hidden items-center gap-2 rounded-full border border-border/70 bg-muted/40 py-1 ps-1 pe-3 sm:flex"
|
||||
>
|
||||
<img
|
||||
class="h-5 w-5 rounded-full"
|
||||
class="h-6 w-6 rounded-full ring-1 ring-border"
|
||||
src="/images/country-flags/{(location.country_code || 'united_nations').toLowerCase()}.svg"
|
||||
alt={location.country}
|
||||
/>
|
||||
<span class="text-sm font-medium text-foreground">
|
||||
<span class="text-sm font-semibold text-foreground">
|
||||
{location.name}
|
||||
</span>
|
||||
{#if location.admin1 || location.country}
|
||||
<span class="text-xs text-muted-foreground">
|
||||
<span class="hidden text-xs text-muted-foreground lg:inline">
|
||||
{#if location.admin1}{location.admin1},{/if}
|
||||
{location.country}
|
||||
</span>
|
||||
@@ -75,7 +90,7 @@
|
||||
<!-- Spacer -->
|
||||
<div class="flex-1"></div>
|
||||
|
||||
<!-- Location search -->
|
||||
<!-- Location search: primary way to switch places, so keep it loud -->
|
||||
<div class="w-full max-w-sm md:max-w-md">
|
||||
<LocationSearch
|
||||
label="Search location..."
|
||||
@@ -84,6 +99,58 @@
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 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"
|
||||
onclick={cycleTheme}
|
||||
title={themeTitles[$storedTheme]}
|
||||
aria-label={themeTitles[$storedTheme]}
|
||||
>
|
||||
{#if $storedTheme === 'light'}
|
||||
<!-- sun -->
|
||||
<svg
|
||||
class="h-4.5 w-4.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.75"
|
||||
>
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
d="M12 2v2m0 16v2M4.93 4.93l1.41 1.41m11.32 11.32 1.41 1.41M2 12h2m16 0h2M4.93 19.07l1.41-1.41m11.32-11.32 1.41-1.41"
|
||||
/>
|
||||
</svg>
|
||||
{:else if $storedTheme === 'dark'}
|
||||
<!-- moon -->
|
||||
<svg
|
||||
class="h-4.5 w-4.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.75"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<!-- monitor (system) -->
|
||||
<svg
|
||||
class="h-4.5 w-4.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.75"
|
||||
>
|
||||
<rect x="3" y="4" width="18" height="13" rx="2" />
|
||||
<path stroke-linecap="round" d="M8 21h8m-4-4v4" />
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -66,22 +66,27 @@
|
||||
<div
|
||||
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary text-primary-foreground"
|
||||
>
|
||||
<!-- umbrella-with-rain logo mark (matches favicon) -->
|
||||
<svg
|
||||
class="h-4.5 w-4.5"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke-width="1.75"
|
||||
>
|
||||
<!-- rain falls from above onto the canopy -->
|
||||
<path stroke-linecap="round" d="M4.5 3v.01M19.5 3v.01M8.5 1.5v.01M15.5 2.5v.01" />
|
||||
<path stroke-linecap="round" d="M12 4.5V6" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z"
|
||||
fill="currentColor"
|
||||
stroke="none"
|
||||
d="M4 14a8 8 0 0 1 16 0c-.66-1-1.99-1-2.66 0-.67-1-2-1-2.67 0-.67-1-2-1-2.67 0-.67-1-2-1-2.67 0C8.66 13 7.33 13 6.66 14 6 13 4.66 13 4 14Z"
|
||||
/>
|
||||
<path stroke-linecap="round" d="M12 14v5a1.9 1.9 0 0 1-3.8 0" />
|
||||
</svg>
|
||||
</div>
|
||||
{#if !collapsed}
|
||||
<span class="text-sm font-semibold whitespace-nowrap text-sidebar-foreground">
|
||||
<span class="text-sm font-bold tracking-tight whitespace-nowrap text-sidebar-foreground">
|
||||
OMbrella
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
@@ -43,3 +43,7 @@ export const defaultLocation: GeoLocation = {
|
||||
};
|
||||
|
||||
export const storedLocation = persisted('stored_location', defaultLocation as GeoLocation);
|
||||
|
||||
export type Theme = 'system' | 'light' | 'dark';
|
||||
|
||||
export const storedTheme = persisted<Theme>('theme', 'system');
|
||||
|
||||
Reference in New Issue
Block a user