move settings to one icon mobile

This commit is contained in:
Vincent van der Wal
2026-08-01 12:39:19 +02:00
parent 8ad02ea3f9
commit 5cc9a5428c
23 changed files with 1511 additions and 189 deletions
@@ -23,6 +23,7 @@
{ href: resolve('/weather/week'), label: '7-Day Forecast' },
{ href: resolve('/weather/compare'), label: 'Model Comparison' },
{ href: resolve('/weather/14-day'), label: '14-Day Forecast' },
{ href: resolve('/weather/seasonal'), label: 'Seasonal Outlook' },
{ href: resolve('/weather/historical'), label: 'Historical Weather' },
{ href: resolve('/weather/maps'), label: 'Weather Maps' }
];
+29 -56
View File
@@ -12,7 +12,10 @@
import LocationSearch from '$lib/components/location/location-search.svelte';
import UnitSelector from '$lib/components/unit-selector.svelte';
import PremiumBadge from '$lib/paywall/PremiumBadge.svelte';
import SupporterBadge from '$lib/paywall/SupporterBadge.svelte';
import SettingsMenu from './settings-menu.svelte';
import ThemeIcon from './theme-icon.svelte';
interface Props {
onMenuToggle?: () => void;
@@ -60,6 +63,8 @@
goto(resolve('/weather/14-day/[location]', { location: locationRoute }));
} else if (currentPath.startsWith('/weather/historical')) {
goto(resolve('/weather/historical/[location]', { location: locationRoute }));
} else if (currentPath.startsWith('/weather/seasonal')) {
goto(resolve('/weather/seasonal/[location]', { location: locationRoute }));
} else {
goto(resolve('/weather/week/[location]', { location: locationRoute }));
}
@@ -117,63 +122,31 @@
/>
</div>
<!-- Measurement units -->
<UnitSelector />
<!-- Phones only have room for one control, so units, theme and supporter
status collapse into a single settings menu below md. -->
<div class="md:hidden">
<SettingsMenu />
</div>
<!-- Premium status / unlock -->
<PremiumBadge />
<!-- md+: the same settings as individual controls. Kept mounted (not `{#if}`)
so SupporterBadge still verifies the key on every viewport. -->
<div class="hidden items-center gap-3 md:flex">
<!-- 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"
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>
<!-- Supporter status / unlock -->
<SupporterBadge />
<!-- 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]}
>
<ThemeIcon theme={$storedTheme} />
</button>
</div>
</header>
<style>
@@ -0,0 +1,123 @@
<script lang="ts">
import { type Theme, storedTheme } from '$lib/stores/settings';
import * as Popover from '$lib/components/ui/popover';
import UnitOptions from '$lib/components/unit-options.svelte';
import UnlockDialog from '$lib/paywall/UnlockDialog.svelte';
import { isSupporter } from '$lib/paywall/supporter';
import ThemeIcon from './theme-icon.svelte';
// The topbar has room for one control on a phone, so units, theme and the
// supporter status share this menu instead of each carrying its own trigger.
const THEMES: { value: Theme; label: string }[] = [
{ value: 'system', label: 'System' },
{ value: 'light', label: 'Light' },
{ value: 'dark', label: 'Dark' }
];
let open = $state(false);
let unlockOpen = $state(false);
</script>
<Popover.Root bind:open>
<Popover.Trigger
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 data-[state=open]:bg-muted data-[state=open]:text-foreground"
aria-label="Settings"
title="Settings"
>
<!-- gear -->
<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="3" />
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-2.9 1.2v.2a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.9.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0-1.2-2.9H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.9l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.9.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 2.9 1.2l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.9V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1Z"
/>
</svg>
</Popover.Trigger>
<Popover.Content align="end" class="w-72 border-border">
<div class="flex flex-col gap-4">
<UnitOptions />
<div>
<span
class="mb-1.5 block text-[11px] font-semibold tracking-wide text-muted-foreground uppercase"
>
Theme
</span>
<div class="flex gap-1 rounded-lg bg-muted p-0.5">
{#each THEMES as option (option.value)}
{@const active = $storedTheme === option.value}
<button
class="flex flex-1 cursor-pointer items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-[13px] font-semibold transition-colors {active
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'}"
aria-pressed={active}
onclick={() => storedTheme.set(option.value)}
>
<ThemeIcon theme={option.value} class="h-4 w-4" />
{option.label}
</button>
{/each}
</div>
</div>
<div class="border-t border-border/70 pt-3">
<button
type="button"
class="flex w-full cursor-pointer items-center gap-2.5 rounded-lg px-2 py-2 text-left transition-colors hover:bg-muted"
onclick={() => {
open = false;
unlockOpen = true;
}}
>
{#if $isSupporter}
<!-- star -->
<svg
class="h-4.5 w-4.5 shrink-0 text-amber-500"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
>
<path
d="M12 2.5l2.9 6 6.6.9-4.8 4.6 1.2 6.5L12 17.9 6.1 20.5l1.2-6.5L2.5 9.4l6.6-.9L12 2.5z"
/>
</svg>
{:else}
<!-- padlock -->
<svg
class="h-4.5 w-4.5 shrink-0 text-muted-foreground"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
stroke-width="1.75"
aria-hidden="true"
>
<rect x="4" y="10" width="16" height="11" rx="2" />
<path stroke-linecap="round" d="M8 10V7a4 4 0 0 1 8 0v3" />
</svg>
{/if}
<span class="min-w-0 flex-1">
<span class="block text-[13px] font-semibold">
{$isSupporter ? 'Supporter extras active' : 'Support Drizz.li'}
</span>
<span class="block text-[11px] text-muted-foreground">
{$isSupporter ? 'Manage your access key' : 'Unlock the supporter extras'}
</span>
</span>
</button>
</div>
</div>
</Popover.Content>
</Popover.Root>
<UnlockDialog bind:open={unlockOpen} />
@@ -0,0 +1,36 @@
<script lang="ts">
import type { Theme } from '$lib/stores/settings';
interface Props {
theme: Theme;
class?: string;
}
let { theme, class: className = 'h-4.5 w-4.5' }: Props = $props();
</script>
{#if theme === 'light'}
<!-- sun -->
<svg class={className} 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 theme === 'dark'}
<!-- moon -->
<svg class={className} 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={className} 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}
@@ -30,6 +30,12 @@
'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'
]
},
{
title: 'Seasonal',
url: '/weather/seasonal' as const,
// rising trend line (long-range outlook)
iconPaths: ['M3 17l6-6 4 4 7-7', 'M16 8h5v5']
},
{
title: 'Historical',
url: '/weather/historical' as const,
+67
View File
@@ -0,0 +1,67 @@
<script lang="ts">
import { type UnitPrefs, storedUnits } from '$lib/stores/settings';
// each group maps a stored unit key to its selectable options
const UNIT_GROUPS: {
key: keyof UnitPrefs;
label: string;
options: { value: string; label: string }[];
}[] = [
{
key: 'temperature_unit',
label: 'Temperature',
options: [
{ value: 'celsius', label: '°C' },
{ value: 'fahrenheit', label: '°F' }
]
},
{
key: 'wind_speed_unit',
label: 'Wind speed',
options: [
{ value: 'kmh', label: 'km/h' },
{ value: 'ms', label: 'm/s' },
{ value: 'mph', label: 'mph' },
{ value: 'kn', label: 'kn' }
]
},
{
key: 'precipitation_unit',
label: 'Precipitation',
options: [
{ value: 'mm', label: 'mm' },
{ value: 'inch', label: 'inch' }
]
}
];
function setUnit(key: keyof UnitPrefs, value: string) {
storedUnits.update((u) => ({ ...u, [key]: value }));
}
</script>
<div class="flex flex-col gap-4">
{#each UNIT_GROUPS as group (group.key)}
<div>
<span
class="mb-1.5 block text-[11px] font-semibold tracking-wide text-muted-foreground uppercase"
>
{group.label}
</span>
<div class="flex gap-1 rounded-lg bg-muted p-0.5">
{#each group.options as opt (opt.value)}
{@const active = $storedUnits[group.key] === opt.value}
<button
class="flex-1 cursor-pointer rounded-md px-2 py-1.5 text-[13px] font-semibold transition-colors {active
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'}"
aria-pressed={active}
onclick={() => setUnit(group.key, opt.value)}
>
{opt.label}
</button>
{/each}
</div>
</div>
{/each}
</div>
+2 -65
View File
@@ -1,45 +1,6 @@
<script lang="ts">
import { type UnitPrefs, storedUnits } from '$lib/stores/settings';
import * as Popover from '$lib/components/ui/popover';
// each group maps a stored unit key to its selectable options
const UNIT_GROUPS: {
key: keyof UnitPrefs;
label: string;
options: { value: string; label: string }[];
}[] = [
{
key: 'temperature_unit',
label: 'Temperature',
options: [
{ value: 'celsius', label: '°C' },
{ value: 'fahrenheit', label: '°F' }
]
},
{
key: 'wind_speed_unit',
label: 'Wind speed',
options: [
{ value: 'kmh', label: 'km/h' },
{ value: 'ms', label: 'm/s' },
{ value: 'mph', label: 'mph' },
{ value: 'kn', label: 'kn' }
]
},
{
key: 'precipitation_unit',
label: 'Precipitation',
options: [
{ value: 'mm', label: 'mm' },
{ value: 'inch', label: 'inch' }
]
}
];
function setUnit(key: keyof UnitPrefs, value: string) {
storedUnits.update((u) => ({ ...u, [key]: value }));
}
import UnitOptions from '$lib/components/unit-options.svelte';
</script>
<Popover.Root>
@@ -65,30 +26,6 @@
</svg>
</Popover.Trigger>
<Popover.Content align="end" class="w-64 border-border">
<div class="flex flex-col gap-4">
{#each UNIT_GROUPS as group (group.key)}
<div>
<span
class="mb-1.5 block text-[11px] font-semibold tracking-wide text-muted-foreground uppercase"
>
{group.label}
</span>
<div class="flex gap-1 rounded-lg bg-muted p-0.5">
{#each group.options as opt (opt.value)}
{@const active = $storedUnits[group.key] === opt.value}
<button
class="flex-1 cursor-pointer rounded-md px-2 py-1.5 text-[13px] font-semibold transition-colors {active
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'}"
aria-pressed={active}
onclick={() => setUnit(group.key, opt.value)}
>
{opt.label}
</button>
{/each}
</div>
</div>
{/each}
</div>
<UnitOptions />
</Popover.Content>
</Popover.Root>