1 Commits

Author SHA1 Message Date
vcoppe 7d3423f9ad New translations settings.mdx (Greek) 2026-04-02 18:54:06 +02:00
395 changed files with 6647 additions and 7259 deletions
+2 -3
View File
@@ -70,9 +70,8 @@ This project has been made possible thanks to the following open source projects
- [SortableJS](https://github.com/SortableJS/Sortable) — creating a sortable file tree - [SortableJS](https://github.com/SortableJS/Sortable) — creating a sortable file tree
- Mapping: - Mapping:
- [MapLibre GL JS](https://github.com/maplibre/maplibre-gl-js) — beautiful and fast interactive map rendering - [MapLibre GL JS](https://github.com/maplibre/maplibre-gl-js) — beautiful and fast interactive map rendering
- [GraphHopper](https://github.com/graphhopper/graphhopper) — powerful routing engine - [GraphHopper](https://github.com/graphhopper/graphhopper) — routing engine
- [OpenStreetMap](https://www.openstreetmap.org) — open map data used by most of the map layers, and by the routing engine - [OpenStreetMap](https://www.openstreetmap.org) — map data used by most of the map layers, and by the routing engine
- [Mapterhorn](https://github.com/mapterhorn/mapterhorn) — high-quality open terrain data used by some map layers (including for 3D), and by the routing engine
- Search: - Search:
- [DocSearch](https://github.com/algolia/docsearch) — search engine for the documentation - [DocSearch](https://github.com/algolia/docsearch) — search engine for the documentation
+3 -6
View File
@@ -1,8 +1,9 @@
{ {
"$schema": "https://shadcn-svelte.com/schema.json", "$schema": "https://shadcn-svelte.com/schema.json",
"style": "default",
"tailwind": { "tailwind": {
"css": "src/app.css", "css": "src/app.css",
"baseColor": "neutral" "baseColor": "slate"
}, },
"aliases": { "aliases": {
"components": "$lib/components", "components": "$lib/components",
@@ -12,9 +13,5 @@
"lib": "$lib" "lib": "$lib"
}, },
"typescript": true, "typescript": true,
"registry": "https://shadcn-svelte.com/registry", "registry": "https://shadcn-svelte.com/registry"
"style": "nova",
"iconLibrary": "lucide",
"menuColor": "default",
"menuAccent": "subtle"
} }
+3864 -3717
View File
File diff suppressed because it is too large Load Diff
+12 -13
View File
@@ -14,9 +14,7 @@
"format": "prettier --write . --config ../.prettierrc --ignore-path ../.prettierignore --ignore-path ./.gitignore" "format": "prettier --write . --config ../.prettierrc --ignore-path ../.prettierignore --ignore-path ./.gitignore"
}, },
"devDependencies": { "devDependencies": {
"@fontsource-variable/inter": "^5.2.8", "@lucide/svelte": "^0.544.0",
"@internationalized/date": "^3.12.0",
"@lucide/svelte": "^1.7.0",
"@sveltejs/adapter-static": "^3.0.8", "@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/enhanced-img": "^0.6.0", "@sveltejs/enhanced-img": "^0.6.0",
"@sveltejs/kit": "^2.21.2", "@sveltejs/kit": "^2.21.2",
@@ -32,8 +30,7 @@
"@types/sortablejs": "^1.15.8", "@types/sortablejs": "^1.15.8",
"@typescript-eslint/eslint-plugin": "^8.33.1", "@typescript-eslint/eslint-plugin": "^8.33.1",
"@typescript-eslint/parser": "^8.33.1", "@typescript-eslint/parser": "^8.33.1",
"bits-ui": "^2.17.2", "bits-ui": "^2.14.4",
"clsx": "^2.1.1",
"eslint": "^9.28.0", "eslint": "^9.28.0",
"eslint-config-prettier": "^10.1.5", "eslint-config-prettier": "^10.1.5",
"eslint-plugin-svelte": "^3.9.1", "eslint-plugin-svelte": "^3.9.1",
@@ -46,37 +43,39 @@
"postcss": "^8.4.47", "postcss": "^8.4.47",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.4.0", "prettier-plugin-svelte": "^3.4.0",
"shadcn-svelte": "^1.2.7",
"svelte": "^5.33.18", "svelte": "^5.33.18",
"svelte-check": "^4.0.0", "svelte-check": "^4.0.0",
"svelte-dnd-action": "^0.9.65", "svelte-dnd-action": "^0.9.65",
"svelte-sonner": "^1.1.0", "svelte-sonner": "^1.0.5",
"tailwind-merge": "^3.5.0", "tailwind-variants": "^3.1.1",
"tailwind-variants": "^3.2.2",
"tailwindcss": "^4.1.8", "tailwindcss": "^4.1.8",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"tsx": "^4.19.1", "tsx": "^4.19.1",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.3.4",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vaul-svelte": "^1.0.0-next.7", "vaul-svelte": "^1.0.0-next.7",
"vite": "^6.3.5" "vite": "^6.3.5",
"vite-plugin-node-polyfills": "^0.23.0"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@docsearch/js": "^3.9.0", "@docsearch/js": "^3.9.0",
"@internationalized/date": "^3.8.2",
"@mapbox/sphericalmercator": "^2.0.1", "@mapbox/sphericalmercator": "^2.0.1",
"@mapbox/tilebelt": "^2.0.2", "@mapbox/tilebelt": "^2.0.2",
"@maplibre/maplibre-gl-geocoder": "^1.9.4", "@maplibre/maplibre-gl-geocoder": "^1.9.4",
"chart.js": "^4.5.1", "chart.js": "^4.5.1",
"chartjs-plugin-zoom": "^2.2.0", "chartjs-plugin-zoom": "^2.2.0",
"clsx": "^2.1.1",
"dexie": "^4.0.11", "dexie": "^4.0.11",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"gpx": "file:../gpx", "gpx": "file:../gpx",
"immer": "^10.1.1", "immer": "^10.1.1",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"mapillary-js": "^4.1.2", "mapillary-js": "^4.1.2",
"maplibre-gl": "^6.0.0", "maplibre-gl": "^5.21.1",
"sanitize-html": "^2.17.0", "sanitize-html": "^2.17.0",
"sortablejs": "^1.15.6" "sortablejs": "^1.15.6",
"tailwind-merge": "^3.3.0"
} }
} }
+53 -91
View File
@@ -1,93 +1,76 @@
@import 'tailwindcss'; @import 'tailwindcss';
@import 'tw-animate-css'; @import 'tw-animate-css';
@import "shadcn-svelte/tailwind.css";
@import "@fontsource-variable/inter";
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
:root { :root {
--background: oklch(1 0 0); --background: hsl(0 0% 100%) /* <- Wrap in HSL */;
--foreground: oklch(0.145 0 0); --foreground: hsl(240 10% 3.9%);
--muted: oklch(0.97 0 0); --muted: hsl(240 4.8% 95.9%);
--muted-foreground: oklch(0.556 0 0); --muted-foreground: hsl(240 3.8% 46.1%);
--popover: oklch(1 0 0); --popover: hsl(0 0% 100%);
--popover-foreground: oklch(0.145 0 0); --popover-foreground: hsl(240 10% 3.9%);
--card: oklch(1 0 0); --card: hsl(0 0% 100%);
--card-foreground: oklch(0.145 0 0); --card-foreground: hsl(240 10% 3.9%);
--border: oklch(0.922 0 0); --border: hsl(240 5.9% 90%);
--input: oklch(0.922 0 0); --input: hsl(240 5.9% 90%);
--primary: oklch(0.205 0 0); --primary: hsl(240 5.9% 10%);
--primary-foreground: oklch(0.985 0 0); --primary-foreground: hsl(0 0% 98%);
--secondary: oklch(0.97 0 0); --secondary: hsl(240 4.8% 95.9%);
--secondary-foreground: oklch(0.205 0 0); --secondary-foreground: hsl(240 5.9% 10%);
--accent: oklch(0.97 0 0); --accent: hsl(240 4.8% 95.9%);
--accent-foreground: oklch(0.205 0 0); --accent-foreground: hsl(240 5.9% 10%);
--destructive: oklch(0.577 0.245 27.325); --destructive: hsl(0 72.2% 50.6%);
--destructive-foreground: hsl(0 0% 98%); --destructive-foreground: hsl(0 0% 98%);
--ring: oklch(0.708 0 0); --ring: hsl(240 10% 3.9%);
--sidebar: oklch(0.985 0 0); --sidebar: hsl(0 0% 98%);
--sidebar-foreground: oklch(0.145 0 0); --sidebar-foreground: hsl(240 5.3% 26.1%);
--sidebar-primary: oklch(0.205 0 0); --sidebar-primary: hsl(240 5.9% 10%);
--sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-primary-foreground: hsl(0 0% 98%);
--sidebar-accent: oklch(0.97 0 0); --sidebar-accent: hsl(240 4.8% 95.9%);
--sidebar-accent-foreground: oklch(0.205 0 0); --sidebar-accent-foreground: hsl(240 5.9% 10%);
--sidebar-border: oklch(0.922 0 0); --sidebar-border: hsl(220 13% 91%);
--sidebar-ring: oklch(0.708 0 0); --sidebar-ring: hsl(217.2 91.2% 59.8%);
--support: rgb(220 15 130); --support: rgb(220 15 130);
--link: rgb(0 110 180); --link: rgb(0 110 180);
--selection: hsl(240 4.8% 93%); --selection: hsl(240 4.8% 93%);
--radius: 0.5rem; --radius: 0.5rem;
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
} }
.dark { .dark {
--background: oklch(0.145 0 0); --background: hsl(240 10% 3.9%);
--foreground: oklch(0.985 0 0); --foreground: hsl(0 0% 98%);
--muted: oklch(0.269 0 0); --muted: hsl(240 3.7% 15.9%);
--muted-foreground: oklch(0.708 0 0); --muted-foreground: hsl(240 5% 64.9%);
--popover: oklch(0.205 0 0); --popover: hsl(240 10% 3.9%);
--popover-foreground: oklch(0.985 0 0); --popover-foreground: hsl(0 0% 98%);
--card: oklch(0.205 0 0); --card: hsl(240 10% 3.9%);
--card-foreground: oklch(0.985 0 0); --card-foreground: hsl(0 0% 98%);
--border: oklch(1 0 0 / 10%); --border: hsl(240 3.7% 15.9%);
--input: oklch(1 0 0 / 15%); --input: hsl(240 3.7% 15.9%);
--primary: oklch(0.922 0 0); --primary: hsl(0 0% 98%);
--primary-foreground: oklch(0.205 0 0); --primary-foreground: hsl(240 5.9% 10%);
--secondary: oklch(0.269 0 0); --secondary: hsl(240 3.7% 15.9%);
--secondary-foreground: oklch(0.985 0 0); --secondary-foreground: hsl(0 0% 98%);
--accent: oklch(0.269 0 0); --accent: hsl(240 3.7% 15.9%);
--accent-foreground: oklch(0.985 0 0); --accent-foreground: hsl(0 0% 98%);
--destructive: oklch(0.704 0.191 22.216); --destructive: hsl(0 62.8% 30.6%);
--destructive-foreground: hsl(0 0% 98%); --destructive-foreground: hsl(0 0% 98%);
--ring: oklch(0.556 0 0); --ring: hsl(240 4.9% 83.9%);
--sidebar: oklch(0.205 0 0); --sidebar: hsl(240 5.9% 10%);
--sidebar-foreground: oklch(0.985 0 0); --sidebar-foreground: hsl(240 4.8% 95.9%);
--sidebar-primary: oklch(0.488 0.243 264.376); --sidebar-primary: hsl(224.3 76.3% 48%);
--sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-primary-foreground: hsl(0 0% 100%);
--sidebar-accent: oklch(0.269 0 0); --sidebar-accent: hsl(240 3.7% 15.9%);
--sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-accent-foreground: hsl(240 4.8% 95.9%);
--sidebar-border: oklch(1 0 0 / 10%); --sidebar-border: hsl(240 3.7% 15.9%);
--sidebar-ring: oklch(0.556 0 0); --sidebar-ring: hsl(217.2 91.2% 59.8%);
--support: rgb(255 110 190); --support: rgb(255 110 190);
--link: rgb(80 190 255); --link: rgb(80 190 255);
--selection: hsl(240 3.7% 22%); --selection: hsl(240 3.7% 22%);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
} }
@theme inline { @theme inline {
@@ -130,35 +113,14 @@
--color-link: var(--link); --color-link: var(--link);
--breakpoint-xs: 540px; --breakpoint-xs: 540px;
--font-sans: 'Inter Variable', sans-serif;
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--radius-2xl: calc(var(--radius) * 1.8);
--radius-3xl: calc(var(--radius) * 2.2);
--radius-4xl: calc(var(--radius) * 2.6);
} }
@layer base { @layer base {
* { * {
@apply border-border outline-ring/50; @apply border-border;
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
html {
@apply font-sans;
}
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 313 KiB

After

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 729 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 525 KiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 710 KiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

+21 -42
View File
@@ -31,32 +31,10 @@ import bikerouterGravel from './custom/bikerouter-gravel.json';
export const maptilerKeyPlaceHolder = 'MAPTILER_KEY'; export const maptilerKeyPlaceHolder = 'MAPTILER_KEY';
export const basemaps: { [key: string]: string | StyleSpecification } = { export const basemaps: { [key: string]: string | StyleSpecification } = {
libertyTopo: 'https://styles.gpx.studio/liberty-topo.json', maptilerStreets: `https://api.maptiler.com/maps/streets-v4/style.json?key=${maptilerKeyPlaceHolder}`,
libertySatellite: 'https://styles.gpx.studio/liberty-satellite.json', maptilerTopo: `https://api.maptiler.com/maps/topo-v4/style.json?key=${maptilerKeyPlaceHolder}`,
osm: 'https://styles.gpx.studio/osm.json', maptilerOutdoors: `https://api.maptiler.com/maps/outdoor-v4/style.json?key=${maptilerKeyPlaceHolder}`,
osmTopo: 'https://styles.gpx.studio/osm-topo.json', maptilerSatellite: `https://api.maptiler.com/maps/hybrid-v4/style.json?key=${maptilerKeyPlaceHolder}`,
esriSatellite: {
version: 8,
sources: {
esriSatellite: {
type: 'raster',
tiles: [
'https://services.arcgisonline.com/arcgis/rest/services/World_Imagery/MapServer/WMTS/tile/1.0.0/World_Imagery/default/default028mm/{z}/{y}/{x}.jpg',
],
tileSize: 256,
maxzoom: 19,
attribution:
'© <a href="https://www.esri.com/" target="_blank">Esri</a>, Vantor, Earthstar Geographics, and the GIS User Community',
},
},
layers: [
{
id: 'esriSatellite',
type: 'raster',
source: 'esriSatellite',
},
],
},
openStreetMap: { openStreetMap: {
version: 8, version: 8,
sources: { sources: {
@@ -799,11 +777,10 @@ export type LayerTreeType = { [key: string]: LayerTreeType | boolean };
export const basemapTree: LayerTreeType = { export const basemapTree: LayerTreeType = {
basemaps: { basemaps: {
world: { world: {
libertyTopo: true, maptilerStreets: true,
libertySatellite: true, maptilerTopo: true,
osm: true, maptilerOutdoors: true,
osmTopo: true, maptilerSatellite: true,
esriSatellite: true,
openStreetMap: true, openStreetMap: true,
openTopoMap: true, openTopoMap: true,
openHikingMap: true, openHikingMap: true,
@@ -936,7 +913,7 @@ export const overpassTree: LayerTreeType = {
}; };
// Default basemap used // Default basemap used
export const defaultBasemap = 'libertyTopo'; export const defaultBasemap = 'maptilerStreets';
// Default overlays used (none) // Default overlays used (none)
export const defaultOverlays: LayerTreeType = { export const defaultOverlays: LayerTreeType = {
@@ -1025,11 +1002,10 @@ export const defaultOverpassQueries: LayerTreeType = {
export const defaultBasemapTree: LayerTreeType = { export const defaultBasemapTree: LayerTreeType = {
basemaps: { basemaps: {
world: { world: {
libertyTopo: true, maptilerStreets: true,
libertySatellite: true, maptilerTopo: true,
osm: true, maptilerOutdoors: true,
osmTopo: true, maptilerSatellite: true,
esriSatellite: false,
openStreetMap: true, openStreetMap: true,
openTopoMap: true, openTopoMap: true,
openHikingMap: true, openHikingMap: true,
@@ -1168,7 +1144,7 @@ export type CustomLayer = {
maxZoom: number; maxZoom: number;
layerType: 'basemap' | 'overlay'; layerType: 'basemap' | 'overlay';
resourceType: 'raster' | 'vector'; resourceType: 'raster' | 'vector';
value: string | StyleSpecification; value: string | maplibregl.StyleSpecification;
}; };
type OverpassQueryData = { type OverpassQueryData = {
@@ -1449,7 +1425,7 @@ export const overpassQueryData: Record<string, OverpassQueryData> = {
color: 'DarkBlue', color: 'DarkBlue',
}, },
tags: { tags: {
railway: ['station', 'halt'], railway: 'station',
}, },
symbol: 'Ground Transportation', symbol: 'Ground Transportation',
}, },
@@ -1487,11 +1463,14 @@ export const overpassQueryData: Record<string, OverpassQueryData> = {
}; };
export const terrainSources: { [key: string]: RasterDEMSourceSpecification } = { export const terrainSources: { [key: string]: RasterDEMSourceSpecification } = {
'maptiler-dem': {
type: 'raster-dem',
url: `https://api.maptiler.com/tiles/terrain-rgb-v2/tiles.json?key=${maptilerKeyPlaceHolder}`,
},
mapterhorn: { mapterhorn: {
type: 'raster-dem', type: 'raster-dem',
url: 'https://tiles.gpx.studio/mapterhorn.json', url: 'https://tiles.mapterhorn.com/tilejson.json',
encoding: 'terrarium',
}, },
}; };
export const defaultTerrainSource = 'mapterhorn'; export const defaultTerrainSource = 'maptiler-dem';
@@ -33,7 +33,7 @@
<Card.Root <Card.Root
class="h-full {orientation === 'vertical' class="h-full {orientation === 'vertical'
? 'min-w-40 sm:min-w-44' ? 'min-w-40 sm:min-w-44'
: 'w-full h-fit my-1'} ring-0 p-0 text-sm sm:text-base bg-transparent" : 'w-full h-fit my-1'} border-none shadow-none p-0 text-sm sm:text-base bg-transparent"
> >
<Card.Content class="h-full p-0"> <Card.Content class="h-full p-0">
<div <div
@@ -73,7 +73,7 @@
</span> </span>
</Tooltip> </Tooltip>
{/if} {/if}
{#if panelHeight > 150 || (orientation === 'horizontal' && panelWidth > 620)} {#if panelHeight > 160 || (orientation === 'horizontal' && panelWidth > 620)}
<Tooltip <Tooltip
label="{i18n._('quantities.time')} ({i18n._('quantities.moving')} / {i18n._( label="{i18n._('quantities.time')} ({i18n._('quantities.moving')} / {i18n._(
'quantities.total' 'quantities.total'
+2 -2
View File
@@ -14,12 +14,12 @@
} = $props(); } = $props();
</script> </script>
<div class="text-[13px] bg-secondary rounded border flex flex-row items-center p-2 {className}"> <div class="text-sm bg-secondary rounded border flex flex-row items-center p-2 {className}">
<CircleQuestionMark size="16" class="w-4 mr-2 shrink-0 grow-0" /> <CircleQuestionMark size="16" class="w-4 mr-2 shrink-0 grow-0" />
<div> <div>
{@render children()} {@render children()}
{#if link} {#if link}
<a href={link} target="_blank" class="text-[13px] text-link hover:underline"> <a href={link} target="_blank" class="text-sm text-link hover:underline">
{i18n._('menu.more')} {i18n._('menu.more')}
</a> </a>
{/if} {/if}
+7 -1
View File
@@ -8,7 +8,7 @@
...others ...others
}: { }: {
iconOnly?: boolean; iconOnly?: boolean;
company?: 'gpx.studio' | 'github' | 'crowdin' | 'facebook' | 'reddit'; company?: 'gpx.studio' | 'maptiler' | 'github' | 'crowdin' | 'facebook' | 'reddit';
[key: string]: any; [key: string]: any;
} = $props(); } = $props();
</script> </script>
@@ -19,6 +19,12 @@
alt="Logo of gpx.studio." alt="Logo of gpx.studio."
{...others} {...others}
/> />
{:else if company === 'maptiler'}
<img
src="{base}/maptiler-logo{mode.current === 'dark' ? '-dark' : ''}.svg"
alt="Logo of Maptiler."
{...others}
/>
{:else if company === 'github'} {:else if company === 'github'}
<svg <svg
role="img" role="img"
+11 -40
View File
@@ -43,8 +43,6 @@
BookOpenText, BookOpenText,
ChartArea, ChartArea,
Maximize, Maximize,
Maximize2,
Minimize2,
} from '@lucide/svelte'; } from '@lucide/svelte';
import { map } from '$lib/components/map/map'; import { map } from '$lib/components/map/map';
import { editMetadata } from '$lib/components/file-list/metadata/utils.svelte'; import { editMetadata } from '$lib/components/file-list/metadata/utils.svelte';
@@ -72,7 +70,7 @@
import { copied, selection } from '$lib/logic/selection'; import { copied, selection } from '$lib/logic/selection';
import { allHidden } from '$lib/logic/hidden'; import { allHidden } from '$lib/logic/hidden';
import { boundsManager } from '$lib/logic/bounds'; import { boundsManager } from '$lib/logic/bounds';
import { tick, onMount } from 'svelte'; import { tick } from 'svelte';
import { allowedPastes } from '$lib/components/file-list/sortable-file-list'; import { allowedPastes } from '$lib/components/file-list/sortable-file-list';
const { const {
@@ -107,23 +105,6 @@
} }
let layerSettingsOpen = $state(false); let layerSettingsOpen = $state(false);
let fullscreen = $state(false);
function toggleFullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen?.();
} else {
document.exitFullscreen?.();
}
}
onMount(() => {
const handler = () => {
fullscreen = document.fullscreenElement !== null;
};
document.addEventListener('fullscreenchange', handler);
return () => document.removeEventListener('fullscreenchange', handler);
});
</script> </script>
<div class="absolute md:top-2 left-0 right-0 z-20 flex flex-row justify-center pointer-events-none"> <div class="absolute md:top-2 left-0 right-0 z-20 flex flex-row justify-center pointer-events-none">
@@ -396,16 +377,6 @@
{i18n._('menu.toggle_3d')} {i18n._('menu.toggle_3d')}
<Shortcut key={i18n._('menu.right_click_drag')} /> <Shortcut key={i18n._('menu.right_click_drag')} />
</Menubar.Item> </Menubar.Item>
<Menubar.Separator />
<Menubar.CheckboxItem checked={fullscreen} onCheckedChange={toggleFullscreen}>
{#if fullscreen}
<Minimize2 size="16" />
{:else}
<Maximize2 size="16" />
{/if}
{i18n._('menu.fullscreen')}
<Shortcut key="F11" />
</Menubar.CheckboxItem>
</Menubar.Content> </Menubar.Content>
</Menubar.Menu> </Menubar.Menu>
<Menubar.Menu> <Menubar.Menu>
@@ -418,7 +389,7 @@
<Menubar.Content class="border-none"> <Menubar.Content class="border-none">
<Menubar.Sub> <Menubar.Sub>
<Menubar.SubTrigger> <Menubar.SubTrigger>
<Ruler size="16" />{i18n._('menu.distance_units')} <Ruler size="16" class="mr-2" />{i18n._('menu.distance_units')}
</Menubar.SubTrigger> </Menubar.SubTrigger>
<Menubar.SubContent> <Menubar.SubContent>
<Menubar.RadioGroup bind:value={$distanceUnits}> <Menubar.RadioGroup bind:value={$distanceUnits}>
@@ -436,7 +407,7 @@
</Menubar.Sub> </Menubar.Sub>
<Menubar.Sub> <Menubar.Sub>
<Menubar.SubTrigger> <Menubar.SubTrigger>
<Zap size="16" />{i18n._('menu.velocity_units')} <Zap size="16" class="mr-2" />{i18n._('menu.velocity_units')}
</Menubar.SubTrigger> </Menubar.SubTrigger>
<Menubar.SubContent> <Menubar.SubContent>
<Menubar.RadioGroup bind:value={$velocityUnits}> <Menubar.RadioGroup bind:value={$velocityUnits}>
@@ -451,7 +422,7 @@
</Menubar.Sub> </Menubar.Sub>
<Menubar.Sub> <Menubar.Sub>
<Menubar.SubTrigger> <Menubar.SubTrigger>
<Thermometer size="16" />{i18n._('menu.temperature_units')} <Thermometer size="16" class="mr-2" />{i18n._('menu.temperature_units')}
</Menubar.SubTrigger> </Menubar.SubTrigger>
<Menubar.SubContent> <Menubar.SubContent>
<Menubar.RadioGroup bind:value={$temperatureUnits}> <Menubar.RadioGroup bind:value={$temperatureUnits}>
@@ -467,7 +438,7 @@
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Sub> <Menubar.Sub>
<Menubar.SubTrigger> <Menubar.SubTrigger>
<Languages size="16" /> <Languages size="16" class="mr-2" />
{i18n._('menu.language')} {i18n._('menu.language')}
</Menubar.SubTrigger> </Menubar.SubTrigger>
<Menubar.SubContent> <Menubar.SubContent>
@@ -483,9 +454,9 @@
<Menubar.Sub> <Menubar.Sub>
<Menubar.SubTrigger> <Menubar.SubTrigger>
{#if mode.current === 'light' || !mode.current} {#if mode.current === 'light' || !mode.current}
<Sun size="16" /> <Sun size="16" class="mr-2" />
{:else} {:else}
<Moon size="16" /> <Moon size="16" class="mr-2" />
{/if} {/if}
{i18n._('menu.mode')} {i18n._('menu.mode')}
</Menubar.SubTrigger> </Menubar.SubTrigger>
@@ -508,7 +479,7 @@
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Sub> <Menubar.Sub>
<Menubar.SubTrigger> <Menubar.SubTrigger>
<PersonStanding size="16" /> <PersonStanding size="16" class="mr-2" />
{i18n._('menu.street_view_source')} {i18n._('menu.street_view_source')}
</Menubar.SubTrigger> </Menubar.SubTrigger>
<Menubar.SubContent> <Menubar.SubContent>
@@ -529,12 +500,12 @@
</Menubar.Content> </Menubar.Content>
</Menubar.Menu> </Menubar.Menu>
</Menubar.Root> </Menubar.Root>
<div class="h-fit flex flex-row items-center"> <div class="h-fit flex flex-row items-center ml-1 gap-1">
<Button <Button
variant="ghost" variant="ghost"
href="./help" href="./help"
target="_blank" target="_blank"
class="cursor-default h-fit rounded-md px-3 py-0.5" class="cursor-default h-fit rounded-sm px-3 py-0.5"
aria-label={i18n._('menu.help')} aria-label={i18n._('menu.help')}
> >
<BookOpenText size="18" class="md:hidden" /> <BookOpenText size="18" class="md:hidden" />
@@ -546,7 +517,7 @@
variant="ghost" variant="ghost"
href="https://opencollective.com/gpxstudio" href="https://opencollective.com/gpxstudio"
target="_blank" target="_blank"
class="cursor-default h-fit rounded-md font-bold text-support hover:text-support px-3 py-0.5" class="cursor-default h-fit rounded-sm font-bold text-support hover:text-support px-3 py-0.5"
aria-label={i18n._('menu.donate')} aria-label={i18n._('menu.donate')}
> >
<HeartHandshake size="18" class="md:hidden" /> <HeartHandshake size="18" class="md:hidden" />
@@ -35,7 +35,7 @@
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
class="w-full flex flex-row gap-1 border-none {side === 'right' class="w-full flex flex-row gap-1 {side === 'right'
? 'justify-between' ? 'justify-between'
: 'justify-start pl-1'} h-fit {nohover : 'justify-start pl-1'} h-fit {nohover
? 'hover:bg-background' ? 'hover:bg-background'
@@ -62,7 +62,7 @@
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
class="w-full flex flex-row gap-1 border-none {side === 'right' class="w-full flex flex-row gap-1 {side === 'right'
? 'justify-between' ? 'justify-between'
: 'justify-start pl-1'} h-fit {nohover ? 'hover:bg-background' : ''}" : 'justify-start pl-1'} h-fit {nohover ? 'hover:bg-background' : ''}"
> >
@@ -1,10 +1,10 @@
<script lang="ts"> <script lang="ts">
import topoMap from '$lib/assets/img/docs/maps/topo.png?enhanced'; import maptilerTopoMap from '$lib/assets/img/home/maptiler-topo.png?enhanced';
import waymarkedMap from '$lib/assets/img/docs/maps/waymarked.png?enhanced'; import waymarkedMap from '$lib/assets/img/home/waymarked.png?enhanced';
</script> </script>
<div class="relative h-80 aspect-square rounded-2xl shadow-xl overflow-clip"> <div class="relative h-80 aspect-square rounded-2xl shadow-xl overflow-clip">
<enhanced:img src={topoMap} alt="Topo map screenshot." class="absolute" /> <enhanced:img src={maptilerTopoMap} alt="MapTiler Topo map screenshot." class="absolute" />
<enhanced:img <enhanced:img
src={waymarkedMap} src={waymarkedMap}
alt="Waymarked Trails map screenshot." alt="Waymarked Trails map screenshot."
@@ -81,18 +81,20 @@
</ButtonWithTooltip> </ButtonWithTooltip>
</Popover.Trigger> </Popover.Trigger>
<Popover.Content <Popover.Content
class="w-fit p-0 flex flex-col gap-0 overflow-hidden" class="w-fit p-0 flex flex-col"
side="top" side="top"
align="end" align="end"
sideOffset={-32} sideOffset={-32}
> >
<ToggleGroup.Root <ToggleGroup.Root
class="flex flex-col w-full border-none" class="flex flex-col items-start gap-0 p-1 w-full border-none"
type="single" type="single"
size="sm"
bind:value={$elevationFill} bind:value={$elevationFill}
> >
<ToggleGroup.Item value="slope" class="w-full flex flex-row justify-start"> <ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="slope"
>
<div class="w-6 flex justify-center items-center"> <div class="w-6 flex justify-center items-center">
{#if $elevationFill === 'slope'} {#if $elevationFill === 'slope'}
<Circle class="size-1.5 fill-current text-current" /> <Circle class="size-1.5 fill-current text-current" />
@@ -102,8 +104,9 @@
{i18n._('quantities.slope')} {i18n._('quantities.slope')}
</ToggleGroup.Item> </ToggleGroup.Item>
<ToggleGroup.Item <ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="surface" value="surface"
class="w-full flex flex-row justify-start" variant="outline"
> >
<div class="w-6 flex justify-center items-center"> <div class="w-6 flex justify-center items-center">
{#if $elevationFill === 'surface'} {#if $elevationFill === 'surface'}
@@ -114,8 +117,9 @@
{i18n._('quantities.surface')} {i18n._('quantities.surface')}
</ToggleGroup.Item> </ToggleGroup.Item>
<ToggleGroup.Item <ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="highway" value="highway"
class="w-full flex flex-row justify-start" variant="outline"
> >
<div class="w-6 flex justify-center items-center"> <div class="w-6 flex justify-center items-center">
{#if $elevationFill === 'highway'} {#if $elevationFill === 'highway'}
@@ -128,12 +132,14 @@
</ToggleGroup.Root> </ToggleGroup.Root>
<Separator /> <Separator />
<ToggleGroup.Root <ToggleGroup.Root
class="flex flex-col gap-0" class="flex flex-col items-start gap-0 p-1"
type="multiple" type="multiple"
size="sm"
bind:value={$additionalDatasets} bind:value={$additionalDatasets}
> >
<ToggleGroup.Item value="speed" class="w-full flex flex-row justify-start"> <ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="speed"
>
<div class="w-6 flex justify-center items-center"> <div class="w-6 flex justify-center items-center">
{#if $additionalDatasets.includes('speed')} {#if $additionalDatasets.includes('speed')}
<Check size="14" /> <Check size="14" />
@@ -144,7 +150,10 @@
? i18n._('quantities.speed') ? i18n._('quantities.speed')
: i18n._('quantities.pace')} : i18n._('quantities.pace')}
</ToggleGroup.Item> </ToggleGroup.Item>
<ToggleGroup.Item value="hr" class="w-full flex flex-row justify-start"> <ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="hr"
>
<div class="w-6 flex justify-center items-center"> <div class="w-6 flex justify-center items-center">
{#if $additionalDatasets.includes('hr')} {#if $additionalDatasets.includes('hr')}
<Check size="14" /> <Check size="14" />
@@ -153,7 +162,10 @@
<HeartPulse size="15" /> <HeartPulse size="15" />
{i18n._('quantities.heartrate')} {i18n._('quantities.heartrate')}
</ToggleGroup.Item> </ToggleGroup.Item>
<ToggleGroup.Item value="cad" class="w-full flex flex-row justify-start"> <ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="cad"
>
<div class="w-6 flex justify-center items-center"> <div class="w-6 flex justify-center items-center">
{#if $additionalDatasets.includes('cad')} {#if $additionalDatasets.includes('cad')}
<Check size="14" /> <Check size="14" />
@@ -162,7 +174,10 @@
<Orbit size="15" /> <Orbit size="15" />
{i18n._('quantities.cadence')} {i18n._('quantities.cadence')}
</ToggleGroup.Item> </ToggleGroup.Item>
<ToggleGroup.Item value="atemp" class="w-full flex flex-row justify-start"> <ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="atemp"
>
<div class="w-6 flex justify-center items-center"> <div class="w-6 flex justify-center items-center">
{#if $additionalDatasets.includes('atemp')} {#if $additionalDatasets.includes('atemp')}
<Check size="14" /> <Check size="14" />
@@ -171,7 +186,10 @@
<Thermometer size="15" /> <Thermometer size="15" />
{i18n._('quantities.temperature')} {i18n._('quantities.temperature')}
</ToggleGroup.Item> </ToggleGroup.Item>
<ToggleGroup.Item value="power" class="w-full flex flex-row justify-start"> <ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="power"
>
<div class="w-6 flex justify-center items-center"> <div class="w-6 flex justify-center items-center">
{#if $additionalDatasets.includes('power')} {#if $additionalDatasets.includes('power')}
<Check size="14" /> <Check size="14" />
@@ -21,7 +21,6 @@
import { selection } from '$lib/logic/selection'; import { selection } from '$lib/logic/selection';
import { untrack } from 'svelte'; import { untrack } from 'svelte';
import { isSelected, toggle } from '$lib/components/map/layer-control/utils'; import { isSelected, toggle } from '$lib/components/map/layer-control/utils';
import { boundsManager } from '$lib/logic/bounds';
let { let {
useHash = true, useHash = true,
@@ -46,6 +45,26 @@
settings.initialize(); settings.initialize();
function applyOptions() { function applyOptions() {
let downloads: Promise<GPXFile | null>[] = getFilesFromEmbeddingOptions(options).map(
(url) => {
return fetch(url)
.then((response) => response.blob())
.then((blob) => new File([blob], url.split('/').pop() ?? url))
.then(loadFile);
}
);
Promise.all(downloads).then((answers) => {
const files = answers.filter((file) => file !== null) as GPXFile[];
let ids: string[] = [];
files.forEach((file, index) => {
let id = `gpx-${index}-embed`;
file._data.id = id;
ids.push(id);
});
fileStateCollection.setEmbeddedFiles(files);
$fileOrder = ids;
selection.selectAll();
});
if (allowedEmbeddingBasemaps.includes(options.basemap)) { if (allowedEmbeddingBasemaps.includes(options.basemap)) {
$currentBasemap = options.basemap; $currentBasemap = options.basemap;
} }
@@ -71,28 +90,6 @@
].filter((dataset) => dataset !== null) ].filter((dataset) => dataset !== null)
); );
elevationFill.set(options.elevation.fill == 'none' ? undefined : options.elevation.fill); elevationFill.set(options.elevation.fill == 'none' ? undefined : options.elevation.fill);
let downloads: Promise<GPXFile | null>[] = getFilesFromEmbeddingOptions(options).map(
(url) => {
return fetch(url)
.then((response) => response.blob())
.then((blob) => new File([blob], url.split('/').pop() ?? url))
.then(loadFile);
}
);
Promise.all(downloads).then((answers) => {
const files = answers.filter((file) => file !== null) as GPXFile[];
let ids: string[] = [];
files.forEach((file, index) => {
let id = `gpx-${index}-embed`;
file._data.id = id;
ids.push(id);
});
fileStateCollection.setEmbeddedFiles(files);
$fileOrder = ids;
selection.selectAll();
boundsManager.fitBoundsOnLoad(ids);
});
} }
$effect(() => { $effect(() => {
@@ -29,7 +29,7 @@ export const defaultEmbeddingOptions = {
key: '', key: '',
files: [], files: [],
ids: [], ids: [],
basemap: 'libertyTopo', basemap: 'maptilerStreets',
elevation: { elevation: {
show: true, show: true,
height: 170, height: 170,
@@ -126,7 +126,7 @@ export function convertOldEmbeddingOptions(options: URLSearchParams): any {
if (options.has('source')) { if (options.has('source')) {
let basemap = options.get('source')!; let basemap = options.get('source')!;
if (basemap === 'satellite') { if (basemap === 'satellite') {
newOptions.basemap = 'libertySatellite'; newOptions.basemap = 'maptilerSatellite';
} else if (basemap === 'otm') { } else if (basemap === 'otm') {
newOptions.basemap = 'openTopoMap'; newOptions.basemap = 'openTopoMap';
} else if (basemap === 'ohm') { } else if (basemap === 'ohm') {
@@ -114,10 +114,10 @@
<ContextMenu.Trigger class="grow truncate"> <ContextMenu.Trigger class="grow truncate">
<Button <Button
variant="ghost" variant="ghost"
class="relative w-full p-0 overflow-hidden border-none focus-visible:ring-0 focus-visible:ring-offset-0 flex flex-row {orientation === class="relative w-full p-0 overflow-hidden focus-visible:ring-0 focus-visible:ring-offset-0 {orientation ===
'vertical' 'vertical'
? 'h-7' ? 'h-fit'
: 'h-9 px-1.5'} pointer-events-auto" : 'h-9 px-1.5 shadow-md'} pointer-events-auto"
> >
{#if item instanceof ListFileItem || item instanceof ListTrackItem} {#if item instanceof ListFileItem || item instanceof ListTrackItem}
<MetadataDialog bind:open={openEditMetadata} {node} {item} /> <MetadataDialog bind:open={openEditMetadata} {node} {item} />
@@ -126,7 +126,7 @@
{#if item.level === ListLevel.FILE || item.level === ListLevel.TRACK} {#if item.level === ListLevel.FILE || item.level === ListLevel.TRACK}
<div <div
class="absolute {orientation === 'vertical' class="absolute {orientation === 'vertical'
? 'top-0 bottom-0 right-0 w-1' ? 'top-0 bottom-0 right-1 w-1'
: 'top-0 h-1 left-0 right-0'}" : 'top-0 h-1 left-0 right-0'}"
style="background:linear-gradient(to {orientation === 'vertical' style="background:linear-gradient(to {orientation === 'vertical'
? 'bottom' ? 'bottom'
@@ -139,7 +139,7 @@
></div> ></div>
{/if} {/if}
<span <span
class="grow text-left truncate ml-1 flex flex-row items-center {hidden class="w-full text-left truncate py-1 flex flex-row items-center {hidden
? 'text-muted-foreground' ? 'text-muted-foreground'
: ''} {$cut && $copied?.some((i) => i.getFullId() === item.getFullId()) : ''} {$cut && $copied?.some((i) => i.getFullId() === item.getFullId())
? 'text-muted-foreground' ? 'text-muted-foreground'
@@ -2,7 +2,6 @@
import CustomControl from './custom-control'; import CustomControl from './custom-control';
import { map } from '$lib/components/map/map'; import { map } from '$lib/components/map/map';
import { onMount, type Snippet } from 'svelte'; import { onMount, type Snippet } from 'svelte';
import type { Map } from 'maplibre-gl';
let { let {
position = 'top-right', position = 'top-right',
@@ -18,7 +17,7 @@
let control: CustomControl | null = null; let control: CustomControl | null = null;
onMount(() => { onMount(() => {
map.onLoad((map: Map) => { map.onLoad((map: maplibregl.Map) => {
if (position.includes('right')) container.classList.add('float-right'); if (position.includes('right')) container.classList.add('float-right');
else container.classList.add('float-left'); else container.classList.add('float-left');
container.classList.remove('hidden'); container.classList.remove('hidden');
@@ -16,6 +16,7 @@
</script> </script>
<Button <Button
size="sm"
class="justify-start {className}" class="justify-start {className}"
variant="outline" variant="outline"
onclick={() => { onclick={() => {
@@ -39,6 +39,7 @@
/> />
{#if trackpoint.fileId === undefined} {#if trackpoint.fileId === undefined}
<Button <Button
size="sm"
variant="outline" variant="outline"
class="justify-start" class="justify-start"
href={`https://www.openstreetmap.org/edit?#map=${(($map?.getZoom() ?? 17) + 1).toFixed(0)}/${trackpoint.item.getLatitude().toFixed(5)}/${trackpoint.item.getLongitude().toFixed(5)}`} href={`https://www.openstreetmap.org/edit?#map=${(($map?.getZoom() ?? 17) + 1).toFixed(0)}/${trackpoint.item.getLatitude().toFixed(5)}/${trackpoint.item.getLongitude().toFixed(5)}`}
@@ -88,6 +88,7 @@
<CopyCoordinates coordinates={waypoint.item.attributes} /> <CopyCoordinates coordinates={waypoint.item.attributes} />
{#if $currentTool === Tool.WAYPOINT && selected} {#if $currentTool === Tool.WAYPOINT && selected}
<Button <Button
class="p-1 has-[>svg]:px-2 h-8"
variant="outline" variant="outline"
onclick={() => { onclick={() => {
if (waypoint.fileId) { if (waypoint.fileId) {
@@ -74,7 +74,7 @@ export class DistanceMarkers {
layout: { layout: {
'text-field': ['get', 'distance'], 'text-field': ['get', 'distance'],
'text-size': 14, 'text-size': 14,
'text-font': ['Noto Sans Bold'], 'text-font': ['Open Sans Bold'],
}, },
paint: { paint: {
'text-color': 'black', 'text-color': 'black',
@@ -1,10 +1,9 @@
import { MapPopup } from '$lib/components/map/map-popup'; import { MapPopup } from '$lib/components/map/map-popup';
import type { Map } from 'maplibre-gl';
export let waypointPopup: MapPopup | null = null; export let waypointPopup: MapPopup | null = null;
export let trackpointPopup: MapPopup | null = null; export let trackpointPopup: MapPopup | null = null;
export function createPopups(map: Map) { export function createPopups(map: maplibregl.Map) {
removePopups(); removePopups();
waypointPopup = new MapPopup(map, { waypointPopup = new MapPopup(map, {
closeButton: false, closeButton: false,
@@ -1,10 +1,9 @@
import { get, type Readable } from 'svelte/store'; import { get, type Readable } from 'svelte/store';
import { import maplibregl, {
type GeoJSONSource, type GeoJSONSource,
type FilterSpecification, type FilterSpecification,
type MapLayerMouseEvent, type MapLayerMouseEvent,
type MapLayerTouchEvent, type MapLayerTouchEvent,
Point,
} from 'maplibre-gl'; } from 'maplibre-gl';
import { map } from '$lib/components/map/map'; import { map } from '$lib/components/map/map';
import { waypointPopup, trackpointPopup } from './gpx-layer-popup'; import { waypointPopup, trackpointPopup } from './gpx-layer-popup';
@@ -121,7 +120,7 @@ export class GPXLayer {
selected: boolean = false; selected: boolean = false;
currentWaypointData: GeoJSON.FeatureCollection | null = null; currentWaypointData: GeoJSON.FeatureCollection | null = null;
draggedWaypointIndex: number | null = null; draggedWaypointIndex: number | null = null;
draggingStartingPosition: Point = new Point(0, 0); draggingStartingPosition: maplibregl.Point = new maplibregl.Point(0, 0);
unsubscribe: Function[] = []; unsubscribe: Function[] = [];
updateBinded: () => void = this.update.bind(this); updateBinded: () => void = this.update.bind(this);
@@ -252,11 +251,11 @@ export class GPXLayer {
source: this.fileId, source: this.fileId,
layout: { layout: {
'text-field': '»', 'text-field': '»',
'text-offset': [0, -0.1], 'text-offset': [0, -0.06],
'text-keep-upright': false, 'text-keep-upright': false,
'text-max-angle': 361, 'text-max-angle': 361,
'text-allow-overlap': true, 'text-allow-overlap': true,
'text-font': ['Noto Sans Bold'], 'text-font': ['Open Sans Bold'],
'symbol-placement': 'line', 'symbol-placement': 'line',
'symbol-spacing': 20, 'symbol-spacing': 20,
}, },
@@ -232,13 +232,16 @@
<div class="flex flex-col"> <div class="flex flex-col">
{#if $customBasemapOrder.length > 0} {#if $customBasemapOrder.length > 0}
<div class="px-3 py-2">
<div class="flex flex-row items-center gap-1 font-semibold mb-2"> <div class="flex flex-row items-center gap-1 font-semibold mb-2">
<Map size="16" /> <Map size="16" />
{i18n._('layers.label.basemaps')} {i18n._('layers.label.basemaps')}
<div class="grow">
<Separator />
</div> </div>
</div>
{/if}
<div <div
class="ml-1.5 flex flex-col gap-1" class="ml-1.5 flex flex-col gap-1 {$customBasemapOrder.length > 0 ? 'mb-2' : ''}"
use:dndzone={{ use:dndzone={{
items: customBasemapItems, items: customBasemapItems,
type: 'basemap', type: 'basemap',
@@ -255,17 +258,14 @@
onfinalize={(e) => { onfinalize={(e) => {
customBasemapItems = e.detail.items; customBasemapItems = e.detail.items;
$customBasemapOrder = customBasemapItems.map((item) => item.id); $customBasemapOrder = customBasemapItems.map((item) => item.id);
$selectedBasemapTree.basemaps['custom'] = customBasemapItems.reduce( $selectedBasemapTree.basemaps['custom'] = customBasemapItems.reduce((acc, item) => {
(acc, item) => {
acc[item.id] = true; acc[item.id] = true;
return acc; return acc;
}, }, {});
{}
);
}} }}
> >
{#each customBasemapItems as item (item.id)} {#each customBasemapItems as item (item.id)}
<div class="flex flex-row items-center gap-1"> <div class="flex flex-row items-center gap-2">
<Move size="12" /> <Move size="12" />
<span class="grow">{item.name}</span> <span class="grow">{item.name}</span>
<Button <Button
@@ -287,18 +287,17 @@
</div> </div>
{/each} {/each}
</div> </div>
</div>
<Separator />
{/if}
{#if $customOverlayOrder.length > 0} {#if $customOverlayOrder.length > 0}
<div class="px-3 py-2">
<div class="flex flex-row items-center gap-1 font-semibold mb-2"> <div class="flex flex-row items-center gap-1 font-semibold mb-2">
<Layers2 size="16" /> <Layers2 size="16" />
{i18n._('layers.label.overlays')} {i18n._('layers.label.overlays')}
<div class="grow"></div> <div class="grow">
<Separator />
</div> </div>
</div>
{/if}
<div <div
class="ml-1.5 flex flex-col gap-1" class="ml-1.5 flex flex-col gap-1 {$customOverlayOrder.length > 0 ? 'mb-2' : ''}"
use:dndzone={{ use:dndzone={{
items: customOverlayItems, items: customOverlayItems,
type: 'overlay', type: 'overlay',
@@ -315,17 +314,14 @@
onfinalize={(e) => { onfinalize={(e) => {
customOverlayItems = e.detail.items; customOverlayItems = e.detail.items;
$customOverlayOrder = customOverlayItems.map((item) => item.id); $customOverlayOrder = customOverlayItems.map((item) => item.id);
$selectedOverlayTree.overlays['custom'] = customOverlayItems.reduce( $selectedOverlayTree.overlays['custom'] = customOverlayItems.reduce((acc, item) => {
(acc, item) => {
acc[item.id] = true; acc[item.id] = true;
return acc; return acc;
}, }, {});
{}
);
}} }}
> >
{#each customOverlayItems as item (item.id)} {#each customOverlayItems as item (item.id)}
<div class="flex flex-row items-center gap-1"> <div class="flex flex-row items-center gap-2">
<Move size="12" /> <Move size="12" />
<span class="grow">{item.name}</span> <span class="grow">{item.name}</span>
<Button <Button
@@ -347,12 +343,9 @@
</div> </div>
{/each} {/each}
</div> </div>
</div> <Card.Root class="py-0 gap-0 shadow-none">
<Separator />
{/if}
<Card.Root class="py-0 gap-0 shadow-none ring-0">
<Card.Header class="p-3"> <Card.Header class="p-3">
<Card.Title class="text-sm font-semibold"> <Card.Title class="text-base">
{#if selectedLayerId} {#if selectedLayerId}
{i18n._('layers.custom_layers.edit')} {i18n._('layers.custom_layers.edit')}
{:else} {:else}
@@ -360,7 +353,7 @@
{/if} {/if}
</Card.Title> </Card.Title>
</Card.Header> </Card.Header>
<Card.Content class="px-3 py-2"> <Card.Content class="p-3 pt-0">
<fieldset class="flex flex-col gap-2"> <fieldset class="flex flex-col gap-2">
<Label for="name">{i18n._('menu.metadata.name')}</Label> <Label for="name">{i18n._('menu.metadata.name')}</Label>
<Input bind:value={name} id="name" class="h-8" /> <Input bind:value={name} id="name" class="h-8" />
@@ -417,7 +410,7 @@
</div> </div>
</RadioGroup.Root> </RadioGroup.Root>
{#if selectedLayerId} {#if selectedLayerId}
<div class="mt-2 flex flex-row gap-1"> <div class="mt-2 flex flex-row gap-2">
<Button variant="outline" onclick={createLayer} class="grow"> <Button variant="outline" onclick={createLayer} class="grow">
<Save size="16" /> <Save size="16" />
{i18n._('layers.custom_layers.update')} {i18n._('layers.custom_layers.update')}
@@ -7,7 +7,6 @@
import { Layers } from '@lucide/svelte'; import { Layers } from '@lucide/svelte';
import { settings } from '$lib/logic/settings'; import { settings } from '$lib/logic/settings';
import { map } from '$lib/components/map/map'; import { map } from '$lib/components/map/map';
import type { Map } from 'maplibre-gl';
let container: HTMLDivElement; let container: HTMLDivElement;
let overpassLayer: OverpassLayer; let overpassLayer: OverpassLayer;
@@ -22,7 +21,7 @@
selectedOverpassTree, selectedOverpassTree,
} = settings; } = settings;
map.onLoad((_map: Map) => { map.onLoad((_map: maplibregl.Map) => {
if (overpassLayer) { if (overpassLayer) {
overpassLayer.remove(); overpassLayer.remove();
} }
@@ -121,7 +121,7 @@
<Accordion.Root class="flex flex-col" bind:value={accordionValue} type="single"> <Accordion.Root class="flex flex-col" bind:value={accordionValue} type="single">
<Accordion.Item value="layer-selection" class="flex flex-col"> <Accordion.Item value="layer-selection" class="flex flex-col">
<Accordion.Trigger>{i18n._('layers.selection')}</Accordion.Trigger> <Accordion.Trigger>{i18n._('layers.selection')}</Accordion.Trigger>
<Accordion.Content class="grow flex flex-col border rounded-md mb-1.5"> <Accordion.Content class="grow flex flex-col border rounded">
<div class="py-2 pl-3 pr-2"> <div class="py-2 pl-3 pr-2">
<LayerTree <LayerTree
layerTree={basemapTree} layerTree={basemapTree}
@@ -152,9 +152,7 @@
</Accordion.Item> </Accordion.Item>
<Accordion.Item value="overlay-opacity"> <Accordion.Item value="overlay-opacity">
<Accordion.Trigger>{i18n._('layers.opacity')}</Accordion.Trigger> <Accordion.Trigger>{i18n._('layers.opacity')}</Accordion.Trigger>
<Accordion.Content <Accordion.Content class="flex flex-col gap-3 overflow-visible">
class="flex flex-col gap-3 overflow-visible border rounded-md px-3 py-2 mb-1.5"
>
<div class="flex flex-row gap-6 items-center"> <div class="flex flex-row gap-6 items-center">
<Label> <Label>
{i18n._('layers.custom_layers.overlay')} {i18n._('layers.custom_layers.overlay')}
@@ -233,10 +231,10 @@
<Accordion.Item value="custom-layers"> <Accordion.Item value="custom-layers">
<Accordion.Trigger>{i18n._('layers.custom_layers.title')}</Accordion.Trigger <Accordion.Trigger>{i18n._('layers.custom_layers.title')}</Accordion.Trigger
> >
<Accordion.Content <Accordion.Content>
class="flex flex-col overflow-visible border rounded-md p-0 mb-1.5" <ScrollArea>
>
<CustomLayers /> <CustomLayers />
</ScrollArea>
</Accordion.Content> </Accordion.Content>
</Accordion.Item> </Accordion.Item>
<Accordion.Item value="terrain-source"> <Accordion.Item value="terrain-source">
@@ -15,9 +15,7 @@
poi: PopupItem<any>; poi: PopupItem<any>;
} = $props(); } = $props();
let tags: Record<string, string> = $derived( let tags: Record<string, string> = $derived(poi ? JSON.parse(poi.item.tags) : {});
poi ? (typeof poi.item.tags == 'object' ? poi.item.tags : JSON.parse(poi.item.tags)) : {}
);
let name = $derived.by(() => { let name = $derived.by(() => {
if (poi) { if (poi) {
if (tags.name !== undefined && tags.name !== '') { if (tags.name !== undefined && tags.name !== '') {
@@ -90,14 +88,6 @@
<span class="font-mono">{key}</span> <span class="font-mono">{key}</span>
{#if key === 'website' || key.startsWith('website:') || key.endsWith(':website') || key === 'contact:facebook' || key === 'contact:instagram' || key === 'contact:twitter'} {#if key === 'website' || key.startsWith('website:') || key.endsWith(':website') || key === 'contact:facebook' || key === 'contact:instagram' || key === 'contact:twitter'}
<a href={value} target="_blank" class="text-link underline">{value}</a> <a href={value} target="_blank" class="text-link underline">{value}</a>
{:else if key === 'wikipedia' || key.startsWith('wikipedia:') || key.endsWith(':wikipedia')}
<a
href="https://wikipedia.org/wiki/{value}"
target="_blank"
class="text-link underline"
>
{value}
</a>
{:else if key === 'phone' || key === 'contact:phone'} {:else if key === 'phone' || key === 'contact:phone'}
<a href={'tel:' + value} class="text-link underline">{value}</a> <a href={'tel:' + value} class="text-link underline">{value}</a>
{:else if key === 'email' || key === 'contact:email'} {:else if key === 'email' || key === 'contact:email'}
@@ -6,7 +6,7 @@ import { overpassQueryData } from '$lib/assets/layers';
import { MapPopup } from '$lib/components/map/map-popup'; import { MapPopup } from '$lib/components/map/map-popup';
import { settings } from '$lib/logic/settings'; import { settings } from '$lib/logic/settings';
import { db } from '$lib/db'; import { db } from '$lib/db';
import type { Map as MapLibreMap, GeoJSONSource } from 'maplibre-gl'; import type { GeoJSONSource } from 'maplibre-gl';
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style'; import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
import type { MapLayerEventManager } from '$lib/components/map/map-layer-event-manager'; import type { MapLayerEventManager } from '$lib/components/map/map-layer-event-manager';
import { loadSVGIcon } from '$lib/utils'; import { loadSVGIcon } from '$lib/utils';
@@ -24,11 +24,11 @@ liveQuery(() => db.overpassdata.toArray()).subscribe((pois) => {
}); });
export class OverpassLayer { export class OverpassLayer {
overpassUrl = 'https://overpass.gpx.studio/api/interpreter'; overpassUrl = 'https://overpass.private.coffee/api/interpreter';
minZoom = 12; minZoom = 12;
queryZoom = 12; queryZoom = 12;
expirationTime = 7 * 24 * 3600 * 1000; expirationTime = 7 * 24 * 3600 * 1000;
map: MapLibreMap; map: maplibregl.Map;
layerEventManager: MapLayerEventManager; layerEventManager: MapLayerEventManager;
popup: MapPopup; popup: MapPopup;
@@ -40,7 +40,7 @@ export class OverpassLayer {
updateBinded = this.update.bind(this); updateBinded = this.update.bind(this);
onHoverBinded = this.onHover.bind(this); onHoverBinded = this.onHover.bind(this);
constructor(map: MapLibreMap, layerEventManager: MapLayerEventManager) { constructor(map: maplibregl.Map, layerEventManager: MapLayerEventManager) {
this.map = map; this.map = map;
this.layerEventManager = layerEventManager; this.layerEventManager = layerEventManager;
this.popup = new MapPopup(map, { this.popup = new MapPopup(map, {
@@ -1,19 +1,10 @@
import { fileStateCollection } from '$lib/logic/file-state'; import { fileStateCollection } from '$lib/logic/file-state';
import { import maplibregl from 'maplibre-gl';
LngLatBounds,
MapMouseEvent,
MapTouchEvent,
Point,
type Map,
type MapGeoJSONFeature,
type MapLayerMouseEvent,
type MapLayerTouchEvent,
} from 'maplibre-gl';
type MapLayerMouseEventListener = (e: MapLayerMouseEvent) => void; type MapLayerMouseEventListener = (e: maplibregl.MapLayerMouseEvent) => void;
type MapLayerTouchEventListener = (e: MapLayerTouchEvent) => void; type MapLayerTouchEventListener = (e: maplibregl.MapLayerTouchEvent) => void;
type MapLayerListener = { type MapLayerListener = {
features: MapGeoJSONFeature[]; features: maplibregl.MapGeoJSONFeature[];
mousemoves: MapLayerMouseEventListener[]; mousemoves: MapLayerMouseEventListener[];
mouseenters: MapLayerMouseEventListener[]; mouseenters: MapLayerMouseEventListener[];
mouseleaves: MapLayerMouseEventListener[]; mouseleaves: MapLayerMouseEventListener[];
@@ -24,10 +15,10 @@ type MapLayerListener = {
}; };
export class MapLayerEventManager { export class MapLayerEventManager {
private _map: Map; private _map: maplibregl.Map;
private _listeners: Record<string, MapLayerListener> = {}; private _listeners: Record<string, MapLayerListener> = {};
constructor(map: Map) { constructor(map: maplibregl.Map) {
this._map = map; this._map = map;
this._map.on('mousemove', this._handleMouseMove.bind(this)); this._map.on('mousemove', this._handleMouseMove.bind(this));
this._map.on('click', this._handleMouseClick.bind(this, 'click')); this._map.on('click', this._handleMouseClick.bind(this, 'click'));
@@ -150,8 +141,7 @@ export class MapLayerEventManager {
} }
} }
private _handleMouseMove(e: MapMouseEvent) { private _handleMouseMove(e: maplibregl.MapMouseEvent) {
if (e.originalEvent.buttons > 0) return;
const featuresByLayer = this._getRenderedFeaturesByLayer(e); const featuresByLayer = this._getRenderedFeaturesByLayer(e);
Object.keys(this._listeners).forEach((layerId) => { Object.keys(this._listeners).forEach((layerId) => {
const features = featuresByLayer[layerId] || []; const features = featuresByLayer[layerId] || [];
@@ -159,20 +149,29 @@ export class MapLayerEventManager {
if ((features.length == 0) != (listener.features.length == 0)) { if ((features.length == 0) != (listener.features.length == 0)) {
if (features.length > 0) { if (features.length > 0) {
if (listener.mouseenters.length > 0) { if (listener.mouseenters.length > 0) {
const event = new MapMouseEvent('mouseenter', e.target, e.originalEvent, { const event = new maplibregl.MapMouseEvent(
'mouseenter',
e.target,
e.originalEvent,
{
features: featuresByLayer[layerId]!, features: featuresByLayer[layerId]!,
}); }
);
listener.mouseenters.forEach((l) => l(event)); listener.mouseenters.forEach((l) => l(event));
} }
} else { } else {
if (listener.mouseleaves.length > 0) { if (listener.mouseleaves.length > 0) {
const event = new MapMouseEvent('mouseleave', e.target, e.originalEvent); const event = new maplibregl.MapMouseEvent(
'mouseleave',
e.target,
e.originalEvent
);
listener.mouseleaves.forEach((l) => l(event)); listener.mouseleaves.forEach((l) => l(event));
} }
} }
} }
if (features.length > 0 && listener.mousemoves.length > 0) { if (features.length > 0 && listener.mousemoves.length > 0) {
const event = new MapMouseEvent('mousemove', e.target, e.originalEvent, { const event = new maplibregl.MapMouseEvent('mousemove', e.target, e.originalEvent, {
features: featuresByLayer[layerId]!, features: featuresByLayer[layerId]!,
}); });
listener.mousemoves.forEach((l) => l(event)); listener.mousemoves.forEach((l) => l(event));
@@ -181,39 +180,49 @@ export class MapLayerEventManager {
}); });
} }
private _handleMouseClick(type: string, e: MapMouseEvent) { private _handleMouseClick(type: string, e: maplibregl.MapMouseEvent) {
const featuresByLayer = this._getRenderedFeaturesByLayer(e); const featuresByLayer = this._getRenderedFeaturesByLayer(e);
Object.keys(this._listeners).forEach((layerId) => { Object.keys(this._listeners).forEach((layerId) => {
const features = featuresByLayer[layerId] || []; const features = featuresByLayer[layerId] || [];
const listener = this._listeners[layerId]; const listener = this._listeners[layerId];
if (features.length > 0) { if (features.length > 0) {
if (type === 'click' && listener.clicks.length > 0) { if (type === 'click' && listener.clicks.length > 0) {
const event = new MapMouseEvent('click', e.target, e.originalEvent, { const event = new maplibregl.MapMouseEvent('click', e.target, e.originalEvent, {
features: features, features: features,
}); });
listener.clicks.forEach((l) => l(event)); listener.clicks.forEach((l) => l(event));
} else if (type === 'contextmenu' && listener.contextmenus.length > 0) { } else if (type === 'contextmenu' && listener.contextmenus.length > 0) {
const event = new MapMouseEvent('contextmenu', e.target, e.originalEvent, { const event = new maplibregl.MapMouseEvent(
'contextmenu',
e.target,
e.originalEvent,
{
features: features, features: features,
}); }
);
listener.contextmenus.forEach((l) => l(event)); listener.contextmenus.forEach((l) => l(event));
} else if (type === 'mousedown' && listener.mousedowns.length > 0) { } else if (type === 'mousedown' && listener.mousedowns.length > 0) {
const event = new MapMouseEvent('mousedown', e.target, e.originalEvent, { const event = new maplibregl.MapMouseEvent(
'mousedown',
e.target,
e.originalEvent,
{
features: features, features: features,
}); }
);
listener.mousedowns.forEach((l) => l(event)); listener.mousedowns.forEach((l) => l(event));
} }
} }
}); });
} }
private _handleTouchStart(e: MapTouchEvent) { private _handleTouchStart(e: maplibregl.MapTouchEvent) {
const featuresByLayer = this._getRenderedFeaturesByLayer(e); const featuresByLayer = this._getRenderedFeaturesByLayer(e);
Object.keys(this._listeners).forEach((layerId) => { Object.keys(this._listeners).forEach((layerId) => {
const features = featuresByLayer[layerId] || []; const features = featuresByLayer[layerId] || [];
const listener = this._listeners[layerId]; const listener = this._listeners[layerId];
if (features.length > 0) { if (features.length > 0) {
const event: MapLayerTouchEvent = new MapTouchEvent( const event: maplibregl.MapLayerTouchEvent = new maplibregl.MapTouchEvent(
'touchstart', 'touchstart',
e.target, e.target,
e.originalEvent e.originalEvent
@@ -224,15 +233,18 @@ export class MapLayerEventManager {
}); });
} }
private _getBounds(point: Point) { private _getBounds(point: maplibregl.Point) {
const delta = 30; const delta = 30;
return new LngLatBounds( return new maplibregl.LngLatBounds(
this._map.unproject([point.x - delta, point.y + delta]), this._map.unproject([point.x - delta, point.y + delta]),
this._map.unproject([point.x + delta, point.y - delta]) this._map.unproject([point.x + delta, point.y - delta])
); );
} }
private _filterLayersIntersectingBounds(layerIds: string[], bounds: LngLatBounds): string[] { private _filterLayersIntersectingBounds(
layerIds: string[],
bounds: maplibregl.LngLatBounds
): string[] {
let result = layerIds.filter((layerId) => { let result = layerIds.filter((layerId) => {
if (!this._map.getLayer(layerId)) return false; if (!this._map.getLayer(layerId)) return false;
const fileId = layerId.replace('-waypoints', ''); const fileId = layerId.replace('-waypoints', '');
@@ -248,7 +260,7 @@ export class MapLayerEventManager {
return result; return result;
} }
private _getRenderedFeaturesByLayer(e: MapMouseEvent | MapTouchEvent) { private _getRenderedFeaturesByLayer(e: maplibregl.MapMouseEvent | maplibregl.MapTouchEvent) {
const layerIds = this._filterLayersIntersectingBounds( const layerIds = this._filterLayersIntersectingBounds(
Object.keys(this._listeners), Object.keys(this._listeners),
this._getBounds(e.point) this._getBounds(e.point)
@@ -257,7 +269,7 @@ export class MapLayerEventManager {
layerIds.length > 0 layerIds.length > 0
? this._map.queryRenderedFeatures(e.point, { layers: layerIds }) ? this._map.queryRenderedFeatures(e.point, { layers: layerIds })
: []; : [];
const featuresByLayer: Record<string, MapGeoJSONFeature[]> = {}; const featuresByLayer: Record<string, maplibregl.MapGeoJSONFeature[]> = {};
features.forEach((f) => { features.forEach((f) => {
if (!featuresByLayer[f.layer.id]) { if (!featuresByLayer[f.layer.id]) {
featuresByLayer[f.layer.id] = []; featuresByLayer[f.layer.id] = [];
+8 -8
View File
@@ -1,5 +1,5 @@
import { TrackPoint, Waypoint } from 'gpx'; import { TrackPoint, Waypoint } from 'gpx';
import { LngLat, Map, MapMouseEvent, Popup, type PopupOptions } from 'maplibre-gl'; import maplibregl from 'maplibre-gl';
import { mount, tick, unmount } from 'svelte'; import { mount, tick, unmount } from 'svelte';
import { get, writable, type Writable } from 'svelte/store'; import { get, writable, type Writable } from 'svelte/store';
import MapPopupComponent from '$lib/components/map/MapPopup.svelte'; import MapPopupComponent from '$lib/components/map/MapPopup.svelte';
@@ -11,15 +11,15 @@ export type PopupItem<T = Waypoint | TrackPoint | any> = {
}; };
export class MapPopup { export class MapPopup {
map: Map; map: maplibregl.Map;
popup: Popup; popup: maplibregl.Popup;
item: Writable<PopupItem | null> = writable(null); item: Writable<PopupItem | null> = writable(null);
component: ReturnType<typeof mount>; component: ReturnType<typeof mount>;
maybeHideBinded = this.maybeHide.bind(this); maybeHideBinded = this.maybeHide.bind(this);
constructor(map: Map, options?: PopupOptions) { constructor(map: maplibregl.Map, options?: maplibregl.PopupOptions) {
this.map = map; this.map = map;
this.popup = new Popup(options); this.popup = new maplibregl.Popup(options);
this.component = mount(MapPopupComponent, { this.component = mount(MapPopupComponent, {
target: document.body, target: document.body,
props: { props: {
@@ -51,7 +51,7 @@ export class MapPopup {
this.map.on('mousemove', this.maybeHideBinded); this.map.on('mousemove', this.maybeHideBinded);
} }
maybeHide(e: MapMouseEvent) { maybeHide(e: maplibregl.MapMouseEvent) {
const item = get(this.item); const item = get(this.item);
if (item === null) { if (item === null) {
this.hide(); this.hide();
@@ -75,10 +75,10 @@ export class MapPopup {
getCoordinates() { getCoordinates() {
const item = get(this.item); const item = get(this.item);
if (item === null) { if (item === null) {
return new LngLat(0, 0); return new maplibregl.LngLat(0, 0);
} }
return item.item instanceof Waypoint || item.item instanceof TrackPoint return item.item instanceof Waypoint || item.item instanceof TrackPoint
? item.item.getCoordinates() ? item.item.getCoordinates()
: new LngLat(item.item.lon, item.item.lat); : new maplibregl.LngLat(item.item.lon, item.item.lat);
} }
} }
+1 -3
View File
@@ -1,6 +1,5 @@
import * as maplibregl from 'maplibre-gl'; import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css'; import 'maplibre-gl/dist/maplibre-gl.css';
import workerUrl from 'maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url';
import MaplibreGeocoder, { import MaplibreGeocoder, {
type MaplibreGeocoderFeatureResults, type MaplibreGeocoderFeatureResults,
} from '@maplibre/maplibre-gl-geocoder'; } from '@maplibre/maplibre-gl-geocoder';
@@ -40,7 +39,6 @@ export class MapLibreGLMap {
geocoder: boolean, geocoder: boolean,
geolocate: boolean geolocate: boolean
) { ) {
maplibregl.setWorkerUrl(workerUrl);
this._maptilerKey = maptilerKey; this._maptilerKey = maptilerKey;
this._styleManager = new StyleManager(this._mapStore, this._maptilerKey); this._styleManager = new StyleManager(this._mapStore, this._maptilerKey);
const map = new maplibregl.Map({ const map = new maplibregl.Map({
@@ -9,7 +9,6 @@
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import ButtonWithTooltip from '$lib/components/ButtonWithTooltip.svelte'; import ButtonWithTooltip from '$lib/components/ButtonWithTooltip.svelte';
import type { Map } from 'maplibre-gl';
const { streetViewSource } = settings; const { streetViewSource } = settings;
@@ -21,7 +20,7 @@
let container: HTMLElement; let container: HTMLElement;
onMount(() => { onMount(() => {
map.onLoad((map_: Map) => { map.onLoad((map_: maplibregl.Map) => {
googleRedirect = new GoogleRedirect(map_); googleRedirect = new GoogleRedirect(map_);
mapillaryLayer = new MapillaryLayer( mapillaryLayer = new MapillaryLayer(
map_, map_,
@@ -54,7 +53,7 @@
<CustomControl class="w-[29px] h-[29px] shrink-0"> <CustomControl class="w-[29px] h-[29px] shrink-0">
<ButtonWithTooltip <ButtonWithTooltip
variant="ghost" variant="ghost"
class="w-full h-full border-none rounded-sm" class="w-full h-full"
side="left" side="left"
label={i18n._('menu.toggle_street_view')} label={i18n._('menu.toggle_street_view')}
onclick={() => { onclick={() => {
@@ -1,11 +1,10 @@
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor'; import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
import type { Map, MapMouseEvent } from 'maplibre-gl';
export class GoogleRedirect { export class GoogleRedirect {
map: Map; map: maplibregl.Map;
enabled = false; enabled = false;
constructor(map: Map) { constructor(map: maplibregl.Map) {
this.map = map; this.map = map;
} }
@@ -25,7 +24,7 @@ export class GoogleRedirect {
this.map.off('click', this.openStreetView); this.map.off('click', this.openStreetView);
} }
openStreetView(e: MapMouseEvent) { openStreetView(e: maplibregl.MapMouseEvent) {
window.open( window.open(
`https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=${e.lngLat.lat},${e.lngLat.lng}` `https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=${e.lngLat.lat},${e.lngLat.lng}`
); );
@@ -1,10 +1,4 @@
import { import maplibregl, { type LayerSpecification, type VectorSourceSpecification } from 'maplibre-gl';
Map,
Marker,
type LayerSpecification,
type MapLayerMouseEvent,
type VectorSourceSpecification,
} from 'maplibre-gl';
import { Viewer, type ViewerBearingEvent } from 'mapillary-js/dist/mapillary.module'; import { Viewer, type ViewerBearingEvent } from 'mapillary-js/dist/mapillary.module';
import 'mapillary-js/dist/mapillary.css'; import 'mapillary-js/dist/mapillary.css';
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor'; import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
@@ -49,9 +43,9 @@ const mapillaryImageLayer: LayerSpecification = {
}; };
export class MapillaryLayer { export class MapillaryLayer {
map: Map; map: maplibregl.Map;
layerEventManager: MapLayerEventManager; layerEventManager: MapLayerEventManager;
marker: Marker; marker: maplibregl.Marker;
viewer: Viewer; viewer: Viewer;
active = false; active = false;
@@ -62,7 +56,7 @@ export class MapillaryLayer {
onMouseLeaveBinded = this.onMouseLeave.bind(this); onMouseLeaveBinded = this.onMouseLeave.bind(this);
constructor( constructor(
map: Map, map: maplibregl.Map,
layerEventManager: MapLayerEventManager, layerEventManager: MapLayerEventManager,
container: HTMLElement, container: HTMLElement,
popupOpen: { value: boolean } popupOpen: { value: boolean }
@@ -81,7 +75,7 @@ export class MapillaryLayer {
dot.className = 'maplibregl-user-location-dot'; dot.className = 'maplibregl-user-location-dot';
element.appendChild(dot); element.appendChild(dot);
this.marker = new Marker({ this.marker = new maplibregl.Marker({
rotationAlignment: 'map', rotationAlignment: 'map',
element, element,
}); });
@@ -146,7 +140,7 @@ export class MapillaryLayer {
this.popupOpen.value = false; this.popupOpen.value = false;
} }
onMouseEnter(e: MapLayerMouseEvent) { onMouseEnter(e: maplibregl.MapLayerMouseEvent) {
if ( if (
e.features && e.features &&
e.features.length > 0 && e.features.length > 0 &&
+27 -81
View File
@@ -9,17 +9,10 @@ import {
} from '$lib/assets/layers'; } from '$lib/assets/layers';
import { getLayers } from '$lib/components/map/layer-control/utils'; import { getLayers } from '$lib/components/map/layer-control/utils';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import type {
Map,
GeoJSONSourceSpecification,
LayerSpecification,
StyleSpecification,
} from 'maplibre-gl';
const { currentBasemap, currentOverlays, customLayers, opacities, terrainSource, distanceUnits } = const { currentBasemap, currentOverlays, customLayers, opacities, terrainSource } = settings;
settings;
const emptySource: GeoJSONSourceSpecification = { const emptySource: maplibregl.GeoJSONSourceSpecification = {
type: 'geojson', type: 'geojson',
data: { data: {
type: 'FeatureCollection', type: 'FeatureCollection',
@@ -38,18 +31,18 @@ export const ANCHOR_LAYER_KEY = {
waypoints: 'waypoints-end', waypoints: 'waypoints-end',
routingControls: 'routing-controls-end', routingControls: 'routing-controls-end',
}; };
const anchorLayers: LayerSpecification[] = Object.values(ANCHOR_LAYER_KEY).map((id) => ({ const anchorLayers: maplibregl.LayerSpecification[] = Object.values(ANCHOR_LAYER_KEY).map((id) => ({
id: id, id: id,
type: 'symbol', type: 'symbol',
source: 'empty-source', source: 'empty-source',
})); }));
export class StyleManager { export class StyleManager {
private _map: Writable<Map | null>; private _map: Writable<maplibregl.Map | null>;
private _maptilerKey: string; private _maptilerKey: string;
private _pastOverlays: Set<string> = new Set(); private _pastOverlays: Set<string> = new Set();
constructor(map: Writable<Map | null>, maptilerKey: string) { constructor(map: Writable<maplibregl.Map | null>, maptilerKey: string) {
this._map = map; this._map = map;
this._maptilerKey = maptilerKey; this._maptilerKey = maptilerKey;
this._map.subscribe((map_) => { this._map.subscribe((map_) => {
@@ -64,28 +57,18 @@ export class StyleManager {
opacities.subscribe(() => this.updateOverlays()); opacities.subscribe(() => this.updateOverlays());
terrainSource.subscribe(() => this.updateTerrain()); terrainSource.subscribe(() => this.updateTerrain());
customLayers.subscribe(() => this.updateBasemap()); customLayers.subscribe(() => this.updateBasemap());
i18n.subscribe(() => this.updateBasemap());
distanceUnits.subscribe(() => {
const map = get(this._map);
if (map && (map.getLayer('contours_m') || map.getLayer('contours_ft'))) {
this.updateBasemap();
}
});
} }
updateBasemap() { updateBasemap() {
const map_ = get(this._map); const map_ = get(this._map);
if (!map_) return; if (!map_) return;
let basemap = get(currentBasemap); this.buildStyle().then((style) => map_.setStyle(style));
this.buildStyle(basemap).then((style) => {
if (get(currentBasemap) === basemap) map_.setStyle(style);
});
} }
async buildStyle(basemap: string): Promise<StyleSpecification> { async buildStyle(): Promise<maplibregl.StyleSpecification> {
const custom = get(customLayers); const custom = get(customLayers);
const style: StyleSpecification = { const style: maplibregl.StyleSpecification = {
version: 8, version: 8,
projection: { projection: {
type: 'globe', type: 'globe',
@@ -96,31 +79,17 @@ export class StyleManager {
layers: [], layers: [],
}; };
let basemap = get(currentBasemap);
const basemapInfo = basemaps[basemap] ?? custom[basemap]?.value ?? basemaps[defaultBasemap]; const basemapInfo = basemaps[basemap] ?? custom[basemap]?.value ?? basemaps[defaultBasemap];
const basemapStyle = await this.get(basemapInfo);
let basemapStyle = basemaps.openStreetMap as StyleSpecification;
try {
basemapStyle = await this.get(basemapInfo);
for (const source in basemapStyle.sources) {
const src = basemapStyle.sources[source];
if (
src &&
typeof src === 'object' &&
'url' in src &&
typeof src.url === 'string' &&
src.url.includes(maptilerKeyPlaceHolder)
) {
src.url = src.url.replace(maptilerKeyPlaceHolder, this._maptilerKey);
}
}
} catch (e) {
console.error(e.message);
}
this.merge(style, basemapStyle); this.merge(style, basemapStyle);
if (this._maptilerKey !== '') {
const terrain = this.getCurrentTerrain(); const terrain = this.getCurrentTerrain();
style.sources[terrain.source] = terrainSources[terrain.source]; style.sources[terrain.source] = terrainSources[terrain.source];
style.terrain = terrain.exaggeration > 0 ? terrain : undefined; style.terrain = terrain.exaggeration > 0 ? terrain : undefined;
}
style.layers.push(...anchorLayers); style.layers.push(...anchorLayers);
@@ -140,21 +109,16 @@ export class StyleManager {
if (!layers[overlay]) { if (!layers[overlay]) {
if (this._pastOverlays.has(overlay)) { if (this._pastOverlays.has(overlay)) {
const overlayInfo = custom[overlay]?.value ?? overlays[overlay]; const overlayInfo = custom[overlay]?.value ?? overlays[overlay];
try {
const overlayStyle = await this.get(overlayInfo); const overlayStyle = await this.get(overlayInfo);
for (let layer of overlayStyle.layers ?? []) { for (let layer of overlayStyle.layers ?? []) {
if (map_.getLayer(layer.id)) { if (map_.getLayer(layer.id)) {
map_.removeLayer(layer.id); map_.removeLayer(layer.id);
} }
} }
} catch (e) {
// Should not happen
}
this._pastOverlays.delete(overlay); this._pastOverlays.delete(overlay);
} }
} else { } else {
const overlayInfo = custom[overlay]?.value ?? overlays[overlay]; const overlayInfo = custom[overlay]?.value ?? overlays[overlay];
try {
const overlayStyle = await this.get(overlayInfo); const overlayStyle = await this.get(overlayInfo);
const opacity = overlayOpacities[overlay]; const opacity = overlayOpacities[overlay];
@@ -182,16 +146,15 @@ export class StyleManager {
map_.addLayer(layer, ANCHOR_LAYER_KEY.overlays); map_.addLayer(layer, ANCHOR_LAYER_KEY.overlays);
} }
} }
this._pastOverlays.add(overlay); this._pastOverlays.add(overlay);
} catch (e) {
console.error(e.message);
}
} }
} }
} catch (e) {} } catch (e) {}
} }
updateTerrain() { updateTerrain() {
if (this._maptilerKey === '') return;
const map_ = get(this._map); const map_ = get(this._map);
if (!map_) return; if (!map_) return;
@@ -209,13 +172,15 @@ export class StyleManager {
} }
} }
async get(styleInfo: StyleSpecification | string): Promise<StyleSpecification> { async get(
styleInfo: maplibregl.StyleSpecification | string
): Promise<maplibregl.StyleSpecification> {
if (typeof styleInfo === 'string') { if (typeof styleInfo === 'string') {
let styleUrl = styleInfo as string; let styleUrl = styleInfo as string;
const response = await fetch(styleUrl, { cache: 'force-cache' }); if (styleUrl.includes(maptilerKeyPlaceHolder)) {
if (!response.ok) { styleUrl = styleUrl.replace(maptilerKeyPlaceHolder, this._maptilerKey);
throw new Error(`HTTP error fetching style "${styleInfo}": ${response.status}`);
} }
const response = await fetch(styleUrl, { cache: 'force-cache' });
const style = await response.json(); const style = await response.json();
return style; return style;
} else { } else {
@@ -223,42 +188,20 @@ export class StyleManager {
} }
} }
merge(style: StyleSpecification, other: StyleSpecification) { merge(style: maplibregl.StyleSpecification, other: maplibregl.StyleSpecification) {
style.sources = { ...style.sources, ...other.sources }; style.sources = { ...style.sources, ...other.sources };
const units = get(distanceUnits);
for (let layer of other.layers ?? []) { for (let layer of other.layers ?? []) {
if ('source' in layer) {
if (layer.source == 'contours_m' && units === 'imperial') continue;
if (layer.source == 'contours_ft' && units !== 'imperial') continue;
}
if (layer.type === 'symbol' && layer.layout && layer.layout['text-field']) { if (layer.type === 'symbol' && layer.layout && layer.layout['text-field']) {
const textField = layer.layout['text-field']; const textField = layer.layout['text-field'];
if (Array.isArray(textField)) {
if ( if (
textField.length == 4 && Array.isArray(textField) &&
Array.isArray(textField[3]) && textField.length >= 2 &&
textField[3][0] === 'coalesce' &&
Array.isArray(textField[3][1]) &&
textField[3][1][0] === 'get' &&
typeof textField[3][1][1] === 'string' &&
textField[3][1][1].startsWith('name')
) {
// OpenFreeMap styles
layer.layout['text-field'] = [
'coalesce',
['get', `name:${i18n.lang}`],
['get', 'name'],
];
}
if (
textField.length == 3 &&
textField[0] === 'coalesce' && textField[0] === 'coalesce' &&
Array.isArray(textField[1]) && Array.isArray(textField[1]) &&
textField[1][0] === 'get' && textField[1][0] === 'get' &&
typeof textField[1][1] === 'string' && typeof textField[1][1] === 'string' &&
textField[1][1].startsWith('name') textField[1][1].startsWith('name')
) { ) {
// OpenMapTiles styles
layer.layout['text-field'] = [ layer.layout['text-field'] = [
'coalesce', 'coalesce',
['get', `name:${i18n.lang}`], ['get', `name:${i18n.lang}`],
@@ -266,7 +209,6 @@ export class StyleManager {
]; ];
} }
} }
}
style.layers.push(layer); style.layers.push(layer);
} }
if (other.sprite && !style.sprite) { if (other.sprite && !style.sprite) {
@@ -279,6 +221,10 @@ export class StyleManager {
getCurrentTerrain() { getCurrentTerrain() {
const terrain = get(terrainSource); const terrain = get(terrainSource);
const source = terrainSources[terrain];
if (source.url && source.url.includes(maptilerKeyPlaceHolder)) {
source.url = source.url.replace(maptilerKeyPlaceHolder, this._maptilerKey);
}
const map_ = get(this._map); const map_ = get(this._map);
return { return {
source: terrain, source: terrain,
@@ -11,7 +11,7 @@
import Clean from '$lib/components/toolbar/tools/Clean.svelte'; import Clean from '$lib/components/toolbar/tools/Clean.svelte';
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import RoutingControlPopup from '$lib/components/toolbar/tools/routing/RoutingControlPopup.svelte'; import RoutingControlPopup from '$lib/components/toolbar/tools/routing/RoutingControlPopup.svelte';
import { Popup } from 'maplibre-gl'; import maplibregl from 'maplibre-gl';
import { settings } from '$lib/logic/settings'; import { settings } from '$lib/logic/settings';
let { let {
@@ -23,11 +23,11 @@
const { minimizeRoutingMenu } = settings; const { minimizeRoutingMenu } = settings;
let popupElement: HTMLDivElement | undefined = $state(undefined); let popupElement: HTMLDivElement | undefined = $state(undefined);
let popup: Popup | undefined = $derived.by(() => { let popup: maplibregl.Popup | undefined = $derived.by(() => {
if (!popupElement) { if (!popupElement) {
return undefined; return undefined;
} }
let popup = new Popup({ let popup = new maplibregl.Popup({
closeButton: false, closeButton: false,
maxWidth: undefined, maxWidth: undefined,
}); });
@@ -16,7 +16,7 @@
import { getURLForLanguage } from '$lib/utils'; import { getURLForLanguage } from '$lib/utils';
import { Trash2 } from '@lucide/svelte'; import { Trash2 } from '@lucide/svelte';
import { map } from '$lib/components/map/map'; import { map } from '$lib/components/map/map';
import type { GeoJSONSource, LngLat } from 'maplibre-gl'; import type { GeoJSONSource } from 'maplibre-gl';
import { selection } from '$lib/logic/selection'; import { selection } from '$lib/logic/selection';
import { fileActions } from '$lib/logic/file-actions'; import { fileActions } from '$lib/logic/file-actions';
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor'; import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
@@ -29,7 +29,7 @@
let cleanType = $state(CleanType.INSIDE); let cleanType = $state(CleanType.INSIDE);
let deleteTrackpoints = $state(true); let deleteTrackpoints = $state(true);
let deleteWaypoints = $state(true); let deleteWaypoints = $state(true);
let rectangleCoordinates: LngLat[] = $state([]); let rectangleCoordinates: maplibregl.LngLat[] = $state([]);
$effect(() => { $effect(() => {
if ($map) { if ($map) {
@@ -17,7 +17,7 @@
<div class="flex flex-col gap-3 w-full max-w-80 {props.class ?? ''}"> <div class="flex flex-col gap-3 w-full max-w-80 {props.class ?? ''}">
<Button <Button
variant="outline" variant="outline"
class="whitespace-normal h-fit min-h-8 py-1" class="whitespace-normal h-fit"
disabled={!validSelection} disabled={!validSelection}
onclick={() => fileActions.addElevationToSelection()} onclick={() => fileActions.addElevationToSelection()}
> >
@@ -76,7 +76,7 @@
{/if} {/if}
<Button <Button
variant="outline" variant="outline"
class="whitespace-normal h-fit min-h-8 py-1" class="whitespace-normal h-fit"
disabled={(mergeType === MergeType.TRACES && !canMergeTraces) || disabled={(mergeType === MergeType.TRACES && !canMergeTraces) ||
(mergeType === MergeType.CONTENTS && !canMergeContents)} (mergeType === MergeType.CONTENTS && !canMergeContents)}
onclick={() => { onclick={() => {
@@ -185,8 +185,8 @@
<div class="flex flex-col gap-3 w-full max-w-80 {props.class ?? ''}"> <div class="flex flex-col gap-3 w-full max-w-80 {props.class ?? ''}">
<fieldset class="flex flex-col gap-2"> <fieldset class="flex flex-col gap-2">
<div class="flex flex-row gap-1.5 justify-center"> <div class="flex flex-row gap-2 justify-center">
<div class="flex flex-col gap-1 grow"> <div class="flex flex-col gap-2 grow">
<Label for="speed" class="flex flex-row"> <Label for="speed" class="flex flex-row">
<Zap size="16" /> <Zap size="16" />
{#if $velocityUnits === 'speed'} {#if $velocityUnits === 'speed'}
@@ -239,7 +239,7 @@
{/if} {/if}
</div> </div>
</div> </div>
<div class="flex flex-col gap-1 grow"> <div class="flex flex-col gap-2 grow">
<Label for="duration" class="flex flex-row"> <Label for="duration" class="flex flex-row">
<Timer size="16" /> <Timer size="16" />
{i18n._('toolbar.time.total_time')} {i18n._('toolbar.time.total_time')}
@@ -253,12 +253,11 @@
/> />
</div> </div>
</div> </div>
<div class="flex flex-col gap-1">
<Label class="flex flex-row"> <Label class="flex flex-row">
<CirclePlay size="16" /> <CirclePlay size="16" />
{i18n._('toolbar.time.start')} {i18n._('toolbar.time.start')}
</Label> </Label>
<div class="flex flex-row gap-1.5"> <div class="flex flex-row gap-2">
<DatePicker <DatePicker
bind:value={startDate} bind:value={startDate}
disabled={!canUpdate} disabled={!canUpdate}
@@ -280,13 +279,11 @@
}} }}
/> />
</div> </div>
</div>
<div class="flex flex-col gap-1">
<Label class="flex flex-row"> <Label class="flex flex-row">
<CircleStop size="16" /> <CircleStop size="16" />
{i18n._('toolbar.time.end')} {i18n._('toolbar.time.end')}
</Label> </Label>
<div class="flex flex-row gap-1.5"> <div class="flex flex-row gap-2">
<DatePicker <DatePicker
bind:value={endDate} bind:value={endDate}
disabled={!canUpdate} disabled={!canUpdate}
@@ -308,7 +305,6 @@
}} }}
/> />
</div> </div>
</div>
{#if $gpxStatistics.global.time.moving === 0 || $gpxStatistics.global.time.moving === undefined} {#if $gpxStatistics.global.time.moving === 0 || $gpxStatistics.global.time.moving === undefined}
<div class="mt-0.5 flex flex-row gap-1 items-center"> <div class="mt-0.5 flex flex-row gap-1 items-center">
<Checkbox id="artificial-time" bind:checked={artificial} disabled={!canUpdate} /> <Checkbox id="artificial-time" bind:checked={artificial} disabled={!canUpdate} />
@@ -318,11 +314,11 @@
</div> </div>
{/if} {/if}
</fieldset> </fieldset>
<div class="flex flex-row gap-1.5 items-center"> <div class="flex flex-row gap-2 items-center">
<Button <Button
variant="outline" variant="outline"
disabled={!canUpdate} disabled={!canUpdate}
class="grow shrink whitespace-normal h-fit min-h-8 py-1" class="grow whitespace-normal h-fit"
onclick={() => { onclick={() => {
let effectiveSpeed = getSpeed(); let effectiveSpeed = getSpeed();
if ( if (
@@ -14,7 +14,7 @@
let props: { class?: string } = $props(); let props: { class?: string } = $props();
let sliderValue = $state(50); let sliderValue = $state([50]);
const maxTolerance = 10000; const maxTolerance = 10000;
let validSelection = $derived( let validSelection = $derived(
@@ -25,7 +25,7 @@
$effect(() => { $effect(() => {
tolerance.set( tolerance.set(
minTolerance * 2 ** (sliderValue / (100 / Math.log2(maxTolerance / minTolerance))) minTolerance * 2 ** (sliderValue[0] / (100 / Math.log2(maxTolerance / minTolerance)))
); );
}); });
@@ -36,7 +36,7 @@
<div class="flex flex-col gap-3 w-full max-w-80 {props.class ?? ''}"> <div class="flex flex-col gap-3 w-full max-w-80 {props.class ?? ''}">
<div class="p-2"> <div class="p-2">
<Slider bind:value={sliderValue} min={0} max={100} step={1} type="single" /> <Slider bind:value={sliderValue} min={0} max={100} step={1} type="multiple" />
</div> </div>
<Label class="flex flex-row justify-between"> <Label class="flex flex-row justify-between">
<span>{i18n._('toolbar.reduce.tolerance')}</span> <span>{i18n._('toolbar.reduce.tolerance')}</span>
@@ -163,9 +163,6 @@ export class ReducedGPXLayerCollection {
reduce() { reduce() {
let itemsAndPoints = new Map<ListItem, TrackPoint[]>(); let itemsAndPoints = new Map<ListItem, TrackPoint[]>();
this._simplified.forEach(([item, maxPts, points], itemFullId) => { this._simplified.forEach(([item, maxPts, points], itemFullId) => {
if (!get(selection).hasAnyParent(item)) {
return;
}
itemsAndPoints.set( itemsAndPoints.set(
item, item,
points points
@@ -41,7 +41,6 @@
import { fileActions, getFileIds, newGPXFile } from '$lib/logic/file-actions'; import { fileActions, getFileIds, newGPXFile } from '$lib/logic/file-actions';
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor'; import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
import { RoutingControls, routingControls } from './routing-controls'; import { RoutingControls, routingControls } from './routing-controls';
import type { Popup } from 'maplibre-gl';
let { let {
minimized = $bindable(false), minimized = $bindable(false),
@@ -52,7 +51,7 @@
}: { }: {
minimized?: boolean; minimized?: boolean;
minimizable?: boolean; minimizable?: boolean;
popup?: Popup; popup?: maplibregl.Popup;
popupElement?: HTMLDivElement; popupElement?: HTMLDivElement;
class?: string; class?: string;
} = $props(); } = $props();
@@ -192,7 +191,7 @@
<ButtonWithTooltip <ButtonWithTooltip
label={i18n._('toolbar.routing.reverse.tooltip')} label={i18n._('toolbar.routing.reverse.tooltip')}
variant="outline" variant="outline"
class="gap-1 text-xs px-1.5 py-1.5 h-fit" class="gap-1 text-xs"
disabled={!validSelection} disabled={!validSelection}
onclick={fileActions.reverseSelection} onclick={fileActions.reverseSelection}
> >
@@ -201,7 +200,7 @@
<ButtonWithTooltip <ButtonWithTooltip
label={i18n._('toolbar.routing.route_back_to_start.tooltip')} label={i18n._('toolbar.routing.route_back_to_start.tooltip')}
variant="outline" variant="outline"
class="gap-1 text-xs px-1.5 py-1.5 h-fit" class="gap-1 text-xs"
disabled={!validSelection} disabled={!validSelection}
onclick={() => { onclick={() => {
const selected = selection.getOrderedSelection(); const selected = selection.getOrderedSelection();
@@ -237,14 +236,14 @@
<ButtonWithTooltip <ButtonWithTooltip
label={i18n._('toolbar.routing.round_trip.tooltip')} label={i18n._('toolbar.routing.round_trip.tooltip')}
variant="outline" variant="outline"
class="gap-1 text-xs px-1.5 py-1.5 h-fit" class="gap-1 text-xs"
disabled={!validSelection} disabled={!validSelection}
onclick={fileActions.createRoundTripForSelection} onclick={fileActions.createRoundTripForSelection}
> >
<Repeat class="size-3" />{i18n._('toolbar.routing.round_trip.button')} <Repeat class="size-3" />{i18n._('toolbar.routing.round_trip.button')}
</ButtonWithTooltip> </ButtonWithTooltip>
</div> </div>
<div class="w-full flex flex-row gap-1 items-end justify-between"> <div class="w-full flex flex-row gap-2 items-end justify-between">
<Help link={getURLForLanguage(i18n.lang, '/help/toolbar/routing')}> <Help link={getURLForLanguage(i18n.lang, '/help/toolbar/routing')}>
{#if !validSelection} {#if !validSelection}
{i18n._('toolbar.routing.help_no_file')} {i18n._('toolbar.routing.help_no_file')}
@@ -1,12 +1,10 @@
import { distance, type Coordinates, TrackPoint, TrackSegment, Track, projectedPoint } from 'gpx'; import { distance, type Coordinates, TrackPoint, TrackSegment, Track, projectedPoint } from 'gpx';
import { get, writable, type Readable } from 'svelte/store'; import { get, writable, type Readable } from 'svelte/store';
import { import maplibregl, {
type MapMouseEvent, type MapMouseEvent,
type GeoJSONSource, type GeoJSONSource,
type MapLayerMouseEvent, type MapLayerMouseEvent,
type MapLayerTouchEvent, type MapLayerTouchEvent,
Popup,
Point,
} from 'maplibre-gl'; } from 'maplibre-gl';
import { route } from './routing'; import { route } from './routing';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
@@ -52,18 +50,16 @@ export class RoutingControls {
} }
> = new Map(); > = new Map();
anchors: GeoJSON.Feature<GeoJSON.Point, AnchorProperties>[] = []; anchors: GeoJSON.Feature<GeoJSON.Point, AnchorProperties>[] = [];
popup: Popup; popup: maplibregl.Popup;
popupElement: HTMLElement; popupElement: HTMLElement;
fileUnsubscribe: () => void = () => {}; fileUnsubscribe: () => void = () => {};
unsubscribes: Function[] = []; unsubscribes: Function[] = [];
updateControlsBinded: () => void = this.updateControls.bind(this); updateControlsBinded: () => void = this.updateControls.bind(this);
appendAnchorBinded: (e: MapMouseEvent) => void = this.appendAnchor.bind(this); appendAnchorBinded: (e: MapMouseEvent) => void = this.appendAnchor.bind(this);
addIntermediateAnchorBinded: (e: MapMouseEvent) => void = this.addIntermediateAnchor.bind(this);
draggedAnchorIndex: number | null = null; draggedAnchorIndex: number | null = null;
lastDraggedAnchorEventTime: number = 0; draggingStartingPosition: maplibregl.Point = new maplibregl.Point(0, 0);
draggingStartingPosition: Point = new Point(0, 0);
onMouseEnterBinded: () => void = this.onMouseEnter.bind(this); onMouseEnterBinded: () => void = this.onMouseEnter.bind(this);
onMouseLeaveBinded: () => void = this.onMouseLeave.bind(this); onMouseLeaveBinded: () => void = this.onMouseLeave.bind(this);
onClickBinded: (e: MapLayerMouseEvent) => void = this.onClick.bind(this); onClickBinded: (e: MapLayerMouseEvent) => void = this.onClick.bind(this);
@@ -82,14 +78,14 @@ export class RoutingControls {
constructor( constructor(
fileId: string, fileId: string,
file: Readable<GPXFileWithStatistics | undefined>, file: Readable<GPXFileWithStatistics | undefined>,
popup: Popup, popup: maplibregl.Popup,
popupElement: HTMLElement popupElement: HTMLElement
) { ) {
this.fileId = fileId; this.fileId = fileId;
this.file = file; this.file = file;
for (let zoom = MIN_ANCHOR_ZOOM; zoom <= MAX_ANCHOR_ZOOM; zoom++) { for (let zoom = MIN_ANCHOR_ZOOM; zoom <= MAX_ANCHOR_ZOOM; zoom++) {
this.layers.set(zoom, { this.layers.set(zoom, {
id: `routing-controls-${this.fileId}-${zoom}`, id: `routing-controls-${zoom}`,
anchors: [], anchors: [],
}); });
} }
@@ -137,7 +133,6 @@ export class RoutingControls {
map_.on('style.load', this.updateControlsBinded); map_.on('style.load', this.updateControlsBinded);
map_.on('click', this.appendAnchorBinded); map_.on('click', this.appendAnchorBinded);
layerEventManager.on('mousemove', this.fileId, this.showTemporaryAnchorBinded); layerEventManager.on('mousemove', this.fileId, this.showTemporaryAnchorBinded);
layerEventManager.on('click', this.fileId, this.addIntermediateAnchorBinded);
this.fileUnsubscribe = this.file.subscribe(this.updateControlsBinded); this.fileUnsubscribe = this.file.subscribe(this.updateControlsBinded);
} }
@@ -185,7 +180,7 @@ export class RoutingControls {
this.layers.forEach((layer, zoom) => { this.layers.forEach((layer, zoom) => {
try { try {
let source = map_.getSource(layer.id) as GeoJSONSource | undefined; let source = map_.getSource(layer.id) as maplibregl.GeoJSONSource | undefined;
if (source) { if (source) {
source.setData({ source.setData({
type: 'FeatureCollection', type: 'FeatureCollection',
@@ -242,7 +237,6 @@ export class RoutingControls {
map_?.off('style.load', this.updateControlsBinded); map_?.off('style.load', this.updateControlsBinded);
map_?.off('click', this.appendAnchorBinded); map_?.off('click', this.appendAnchorBinded);
layerEventManager?.off('mousemove', this.fileId, this.showTemporaryAnchorBinded); layerEventManager?.off('mousemove', this.fileId, this.showTemporaryAnchorBinded);
layerEventManager?.off('click', this.fileId, this.addIntermediateAnchorBinded);
map_?.off('mousemove', this.updateTemporaryAnchorBinded); map_?.off('mousemove', this.updateTemporaryAnchorBinded);
this.layers.forEach((layer) => { this.layers.forEach((layer) => {
@@ -522,24 +516,20 @@ export class RoutingControls {
}); });
} }
async appendAnchor(e: MapMouseEvent) { async appendAnchor(e: maplibregl.MapMouseEvent) {
// Add a new anchor to the end of the last segment // Add a new anchor to the end of the last segment
if (get(streetViewEnabled) && get(streetViewSource) === 'google') { if (get(streetViewEnabled) && get(streetViewSource) === 'google') {
return; return;
} }
if (
this.draggedAnchorIndex !== null ||
Date.now() - this.lastDraggedAnchorEventTime < 100
) {
// Exit if anchor is being dragged
return;
}
if ( if (
e.target.queryRenderedFeatures(e.point, { e.target.queryRenderedFeatures(e.point, {
layers: [this.fileId, ...[...this.layers.values()].map((layer) => layer.id)], layers: this.layers
.values()
.map((layer) => layer.id)
.toArray(),
}).length }).length
) { ) {
// Clicked on routing control or layer, ignoring // Clicked on routing control, ignoring
return; return;
} }
this.appendAnchorWithCoordinates({ this.appendAnchorWithCoordinates({
@@ -611,15 +601,6 @@ export class RoutingControls {
await this.routeBetweenAnchors([lastAnchor, newAnchor], [lastAnchorPoint, newAnchorPoint]); await this.routeBetweenAnchors([lastAnchor, newAnchor], [lastAnchorPoint, newAnchorPoint]);
} }
addIntermediateAnchor(e: MapMouseEvent) {
e.preventDefault();
if (this.temporaryAnchor !== null) {
this.turnIntoPermanentAnchor();
return;
}
}
getNeighbouringAnchors(anchor: Anchor): [Anchor | null, Anchor | null] { getNeighbouringAnchors(anchor: Anchor): [Anchor | null, Anchor | null] {
let previousAnchor: Anchor | null = null; let previousAnchor: Anchor | null = null;
let nextAnchor: Anchor | null = null; let nextAnchor: Anchor | null = null;
@@ -822,7 +803,7 @@ export class RoutingControls {
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"> `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<circle cx="10" cy="10" r="8" fill="white" stroke="black" stroke-width="2" /> <circle cx="10" cy="10" r="8" fill="white" stroke="black" stroke-width="2" />
</svg>`, </svg>`,
_map.getCanvasContainer().offsetWidth > 1000 ? 56 : 80 _map.getCanvasContainer().offsetWidth > 1000 ? 50 : 80
); );
} }
@@ -840,11 +821,8 @@ export class RoutingControls {
onClick(e: MapLayerMouseEvent) { onClick(e: MapLayerMouseEvent) {
e.preventDefault(); e.preventDefault();
if ( if (this.temporaryAnchor !== null) {
this.draggedAnchorIndex !== null || this.turnIntoPermanentAnchor();
Date.now() - this.lastDraggedAnchorEventTime < 100
) {
// Exit if anchor is being dragged
return; return;
} }
@@ -933,8 +911,6 @@ export class RoutingControls {
lat: e.lngLat.lat, lat: e.lngLat.lat,
lon: e.lngLat.lng, lon: e.lngLat.lng,
}); });
this.lastDraggedAnchorEventTime = Date.now();
} }
onMouseUp(e: MapLayerMouseEvent | MapLayerTouchEvent) { onMouseUp(e: MapLayerMouseEvent | MapLayerTouchEvent) {
@@ -973,7 +949,6 @@ export class RoutingControls {
} }
this.draggedAnchorIndex = null; this.draggedAnchorIndex = null;
this.lastDraggedAnchorEventTime = Date.now();
} }
showTemporaryAnchor(e: MapLayerMouseEvent) { showTemporaryAnchor(e: MapLayerMouseEvent) {
@@ -1101,9 +1076,7 @@ export class RoutingControls {
if (!this.temporaryAnchor) { if (!this.temporaryAnchor) {
return; return;
} }
let source = get(map)?.getSource(`routing-controls-${this.fileId}-0`) as let source = get(map)?.getSource('routing-controls-0') as GeoJSONSource | undefined;
| GeoJSONSource
| undefined;
if (source) { if (source) {
if (this.temporaryAnchor) { if (this.temporaryAnchor) {
source.updateData({ source.updateData({
@@ -1118,9 +1091,7 @@ export class RoutingControls {
return; return;
} }
const map_ = get(map); const map_ = get(map);
let source = map_?.getSource(`routing-controls-${this.fileId}-0`) as let source = map_?.getSource('routing-controls-0') as GeoJSONSource | undefined;
| GeoJSONSource
| undefined;
if (source) { if (source) {
if (this.temporaryAnchor) { if (this.temporaryAnchor) {
source.updateData({ source.updateData({
@@ -17,7 +17,7 @@ export const routingProfiles: { [key: string]: RoutingProfile } = {
gravel_bike: { engine: 'graphhopper', profile: 'gravelbike' }, gravel_bike: { engine: 'graphhopper', profile: 'gravelbike' },
mountain_bike: { engine: 'graphhopper', profile: 'mtb' }, mountain_bike: { engine: 'graphhopper', profile: 'mtb' },
foot: { engine: 'graphhopper', profile: 'foot' }, foot: { engine: 'graphhopper', profile: 'foot' },
motorcycle: { engine: 'graphhopper', profile: 'motorbike' }, motorcycle: { engine: 'graphhopper', profile: 'motorcycle' },
water: { engine: 'brouter', profile: 'river' }, water: { engine: 'brouter', profile: 'river' },
railway: { engine: 'brouter', profile: 'rail' }, railway: { engine: 'brouter', profile: 'rail' },
}; };
@@ -8,13 +8,13 @@ import { get } from 'svelte/store';
import { fileStateCollection } from '$lib/logic/file-state'; import { fileStateCollection } from '$lib/logic/file-state';
import { fileActions } from '$lib/logic/file-actions'; import { fileActions } from '$lib/logic/file-actions';
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor'; import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
import type { Map, GeoJSONSource, MapLayerMouseEvent } from 'maplibre-gl'; import type { GeoJSONSource } from 'maplibre-gl';
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style'; import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
import type { MapLayerEventManager } from '$lib/components/map/map-layer-event-manager'; import type { MapLayerEventManager } from '$lib/components/map/map-layer-event-manager';
import { loadSVGIcon } from '$lib/utils'; import { loadSVGIcon } from '$lib/utils';
export class SplitControls { export class SplitControls {
map: Map; map: maplibregl.Map;
layerEventManager: MapLayerEventManager; layerEventManager: MapLayerEventManager;
unsubscribes: Function[] = []; unsubscribes: Function[] = [];
@@ -22,7 +22,7 @@ export class SplitControls {
layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.bind(this); layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.bind(this);
layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this); layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
constructor(map: Map, layerEventManager: MapLayerEventManager) { constructor(map: maplibregl.Map, layerEventManager: MapLayerEventManager) {
this.map = map; this.map = map;
this.layerEventManager = layerEventManager; this.layerEventManager = layerEventManager;
loadSVGIcon( loadSVGIcon(
@@ -160,7 +160,7 @@ export class SplitControls {
mapCursor.notify(MapCursorState.SPLIT_CONTROL, false); mapCursor.notify(MapCursorState.SPLIT_CONTROL, false);
} }
layerOnClick(e: MapLayerMouseEvent) { layerOnClick(e: maplibregl.MapLayerMouseEvent) {
let coordinates = (e.features![0].geometry as GeoJSON.Point).coordinates; let coordinates = (e.features![0].geometry as GeoJSON.Point).coordinates;
fileActions.split( fileActions.split(
get(splitAs), get(splitAs),
@@ -16,7 +16,7 @@
import { fileActions } from '$lib/logic/file-actions'; import { fileActions } from '$lib/logic/file-actions';
import { map } from '$lib/components/map/map'; import { map } from '$lib/components/map/map';
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor'; import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
import { Marker } from 'maplibre-gl'; import maplibregl from 'maplibre-gl';
import { getSvgForSymbol } from '$lib/components/map/gpx-layer/gpx-layer'; import { getSvgForSymbol } from '$lib/components/map/gpx-layer/gpx-layer';
let props: { let props: {
@@ -41,7 +41,7 @@
}) })
); );
let marker: Marker | null = null; let marker: maplibregl.Marker | null = null;
function reset() { function reset() {
if ($selectedWaypoint) { if ($selectedWaypoint) {
@@ -125,7 +125,7 @@
let element = document.createElement('div'); let element = document.createElement('div');
element.classList.add('w-8', 'h-8'); element.classList.add('w-8', 'h-8');
element.innerHTML = getSvgForSymbol(symbolKey); element.innerHTML = getSvgForSymbol(symbolKey);
marker = new Marker({ marker = new maplibregl.Marker({
element, element,
anchor: 'bottom', anchor: 'bottom',
}) })
@@ -161,17 +161,14 @@
</script> </script>
<div class="flex flex-col gap-3 w-full max-w-96 {props.class ?? ''}"> <div class="flex flex-col gap-3 w-full max-w-96 {props.class ?? ''}">
<fieldset class="flex flex-col gap-1.5"> <fieldset class="flex flex-col gap-2">
<div class="flex flex-col gap-1">
<Label for="name">{i18n._('menu.metadata.name')}</Label> <Label for="name">{i18n._('menu.metadata.name')}</Label>
<Input <Input
bind:value={name} bind:value={name}
id="name" id="name"
class="font-semibold" class="font-semibold h-8"
disabled={!canCreate && !$selectedWaypoint} disabled={!canCreate && !$selectedWaypoint}
/> />
</div>
<div class="flex flex-col gap-1">
<Label for="description">{i18n._('menu.metadata.description')}</Label> <Label for="description">{i18n._('menu.metadata.description')}</Label>
<Textarea <Textarea
bind:value={description} bind:value={description}
@@ -179,12 +176,11 @@
disabled={!canCreate && !$selectedWaypoint} disabled={!canCreate && !$selectedWaypoint}
class="min-h-8 h-8 py-1 px-3 text-sm" class="min-h-8 h-8 py-1 px-3 text-sm"
/> />
</div>
<div class="flex flex-col gap-1">
<Label for="symbol">{i18n._('toolbar.waypoint.icon')}</Label> <Label for="symbol">{i18n._('toolbar.waypoint.icon')}</Label>
<Select.Root bind:value={sym} type="single"> <Select.Root bind:value={sym} type="single">
<Select.Trigger <Select.Trigger
id="symbol" id="symbol"
size="sm"
class="w-full" class="w-full"
disabled={!canCreate && !$selectedWaypoint} disabled={!canCreate && !$selectedWaypoint}
> >
@@ -200,7 +196,7 @@
{/if} {/if}
</span> </span>
</Select.Trigger> </Select.Trigger>
<Select.Content class="max-h-60"> <Select.Content class="max-h-60 overflow-y-scroll">
{#each sortedSymbols as [key, symbol]} {#each sortedSymbols as [key, symbol]}
<Select.Item value={symbol.value}> <Select.Item value={symbol.value}>
<span> <span>
@@ -216,8 +212,6 @@
{/each} {/each}
</Select.Content> </Select.Content>
</Select.Root> </Select.Root>
</div>
<div class="flex flex-col gap-1">
<Label for="link">{i18n._('toolbar.waypoint.link')}</Label> <Label for="link">{i18n._('toolbar.waypoint.link')}</Label>
<Input <Input
bind:value={link} bind:value={link}
@@ -225,9 +219,8 @@
class="h-8" class="h-8"
disabled={!canCreate && !$selectedWaypoint} disabled={!canCreate && !$selectedWaypoint}
/> />
</div> <div class="flex flex-row gap-2">
<div class="flex flex-row gap-1.5"> <div class="grow flex flex-col gap-2">
<div class="grow flex flex-col gap-1">
<Label for="latitude">{i18n._('toolbar.waypoint.latitude')}</Label> <Label for="latitude">{i18n._('toolbar.waypoint.latitude')}</Label>
<Input <Input
bind:value={latitude} bind:value={latitude}
@@ -240,7 +233,7 @@
disabled={!canCreate && !$selectedWaypoint} disabled={!canCreate && !$selectedWaypoint}
/> />
</div> </div>
<div class="grow flex flex-col gap-1"> <div class="grow flex flex-col gap-2">
<Label for="longitude">{i18n._('toolbar.waypoint.longitude')}</Label> <Label for="longitude">{i18n._('toolbar.waypoint.longitude')}</Label>
<Input <Input
bind:value={longitude} bind:value={longitude}
@@ -255,11 +248,11 @@
</div> </div>
</div> </div>
</fieldset> </fieldset>
<div class="flex flex-row gap-1.5 items-center"> <div class="flex flex-row gap-2 items-center">
<Button <Button
variant="outline" variant="outline"
disabled={!canCreate && !$selectedWaypoint} disabled={!canCreate && !$selectedWaypoint}
class="grow shrink h-fit min-h-8 whitespace-normal py-1" class="grow whitespace-normal h-fit"
onclick={createOrUpdateWaypoint} onclick={createOrUpdateWaypoint}
> >
{#if $selectedWaypoint} {#if $selectedWaypoint}
@@ -13,15 +13,10 @@
<AccordionPrimitive.Content <AccordionPrimitive.Content
bind:ref bind:ref
data-slot="accordion-content" data-slot="accordion-content"
class="data-open:animate-accordion-down data-closed:animate-accordion-up text-sm overflow-hidden" class="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
{...restProps} {...restProps}
> >
<div <div class={cn("pb-4 pt-0", className)}>
class={cn(
"pt-0 pb-2.5 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3 [&_p:not(:last-child)]:mb-4",
className
)}
>
{@render children?.()} {@render children?.()}
</div> </div>
</AccordionPrimitive.Content> </AccordionPrimitive.Content>
@@ -12,6 +12,6 @@
<AccordionPrimitive.Item <AccordionPrimitive.Item
bind:ref bind:ref
data-slot="accordion-item" data-slot="accordion-item"
class={cn("not-last:border-b", className)} class={cn("border-b last:border-b-0", className)}
{...restProps} {...restProps}
/> />
@@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Accordion as AccordionPrimitive } from "bits-ui"; import { Accordion as AccordionPrimitive } from "bits-ui";
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
import { cn, type WithoutChild } from "$lib/utils.js"; import { cn, type WithoutChild } from "$lib/utils.js";
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
import ChevronUpIcon from '@lucide/svelte/icons/chevron-up';
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -20,13 +19,14 @@
data-slot="accordion-trigger" data-slot="accordion-trigger"
bind:ref bind:ref
class={cn( class={cn(
"focus-visible:ring-ring/50 focus-visible:border-ring focus-visible:after:border-ring **:data-[slot=accordion-trigger-icon]:text-muted-foreground rounded-lg py-2.5 text-left text-sm font-medium hover:underline focus-visible:ring-3 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 group/accordion-trigger relative flex flex-1 items-start justify-between border border-transparent transition-all outline-none disabled:pointer-events-none disabled:opacity-50", "focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium outline-none transition-all hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
className className
)} )}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
<ChevronDownIcon data-slot="accordion-trigger-icon" class="cn-accordion-trigger-icon pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden" /> <ChevronDownIcon
<ChevronUpIcon data-slot="accordion-trigger-icon" class="cn-accordion-trigger-icon pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline" /> class="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200"
/>
</AccordionPrimitive.Trigger> </AccordionPrimitive.Trigger>
</AccordionPrimitive.Header> </AccordionPrimitive.Header>
@@ -1,11 +1,9 @@
<script lang="ts"> <script lang="ts">
import { Accordion as AccordionPrimitive } from "bits-ui"; import { Accordion as AccordionPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let { let {
ref = $bindable(null), ref = $bindable(null),
value = $bindable(), value = $bindable(),
class: className,
...restProps ...restProps
}: AccordionPrimitive.RootProps = $props(); }: AccordionPrimitive.RootProps = $props();
</script> </script>
@@ -14,6 +12,5 @@
bind:ref bind:ref
bind:value={value as never} bind:value={value as never}
data-slot="accordion" data-slot="accordion"
class={cn("cn-accordion flex w-full flex-col", className)}
{...restProps} {...restProps}
/> />
@@ -1,27 +1,18 @@
<script lang="ts"> <script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { import { buttonVariants } from "$lib/components/ui/button/index.js";
buttonVariants,
type ButtonVariant,
type ButtonSize,
} from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils.js";
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
variant = "default",
size = "default",
...restProps ...restProps
}: AlertDialogPrimitive.ActionProps & { }: AlertDialogPrimitive.ActionProps = $props();
variant?: ButtonVariant;
size?: ButtonSize;
} = $props();
</script> </script>
<AlertDialogPrimitive.Action <AlertDialogPrimitive.Action
bind:ref bind:ref
data-slot="alert-dialog-action" data-slot="alert-dialog-action"
class={cn(buttonVariants({ variant, size }), "cn-alert-dialog-action", className)} class={cn(buttonVariants(), className)}
{...restProps} {...restProps}
/> />
@@ -1,27 +1,18 @@
<script lang="ts"> <script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { import { buttonVariants } from "$lib/components/ui/button/index.js";
buttonVariants,
type ButtonVariant,
type ButtonSize,
} from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils.js";
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
variant = "outline",
size = "default",
...restProps ...restProps
}: AlertDialogPrimitive.CancelProps & { }: AlertDialogPrimitive.CancelProps = $props();
variant?: ButtonVariant;
size?: ButtonSize;
} = $props();
</script> </script>
<AlertDialogPrimitive.Cancel <AlertDialogPrimitive.Cancel
bind:ref bind:ref
data-slot="alert-dialog-cancel" data-slot="alert-dialog-cancel"
class={cn(buttonVariants({ variant, size }), "cn-alert-dialog-cancel", className)} class={cn(buttonVariants({ variant: "outline" }), className)}
{...restProps} {...restProps}
/> />
@@ -1,32 +1,27 @@
<script lang="ts"> <script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import AlertDialogPortal from "./alert-dialog-portal.svelte";
import AlertDialogOverlay from "./alert-dialog-overlay.svelte"; import AlertDialogOverlay from "./alert-dialog-overlay.svelte";
import { cn, type WithoutChild, type WithoutChildrenOrChild } from "$lib/utils.js"; import { cn, type WithoutChild, type WithoutChildrenOrChild } from "$lib/utils.js";
import type { ComponentProps } from "svelte";
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
size = "default",
portalProps, portalProps,
...restProps ...restProps
}: WithoutChild<AlertDialogPrimitive.ContentProps> & { }: WithoutChild<AlertDialogPrimitive.ContentProps> & {
size?: "default" | "sm"; portalProps?: WithoutChildrenOrChild<AlertDialogPrimitive.PortalProps>;
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof AlertDialogPortal>>;
} = $props(); } = $props();
</script> </script>
<AlertDialogPortal {...portalProps}> <AlertDialogPrimitive.Portal {...portalProps}>
<AlertDialogOverlay /> <AlertDialogOverlay />
<AlertDialogPrimitive.Content <AlertDialogPrimitive.Content
bind:ref bind:ref
data-slot="alert-dialog-content" data-slot="alert-dialog-content"
data-size={size}
class={cn( class={cn(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-popover text-popover-foreground ring-foreground/10 gap-4 rounded-xl p-4 ring-1 duration-100 data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 outline-none", "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed left-[50%] top-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className className
)} )}
{...restProps} {...restProps}
/> />
</AlertDialogPortal> </AlertDialogPrimitive.Portal>
@@ -12,6 +12,6 @@
<AlertDialogPrimitive.Description <AlertDialogPrimitive.Description
bind:ref bind:ref
data-slot="alert-dialog-description" data-slot="alert-dialog-description"
class={cn("text-muted-foreground *:[a]:hover:text-foreground text-sm text-balance md:text-pretty *:[a]:underline *:[a]:underline-offset-3", className)} class={cn("text-muted-foreground text-sm", className)}
{...restProps} {...restProps}
/> />
@@ -13,10 +13,7 @@
<div <div
bind:this={ref} bind:this={ref}
data-slot="alert-dialog-footer" data-slot="alert-dialog-footer"
class={cn( class={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
"bg-muted/50 -mx-4 -mb-4 rounded-b-xl border-t p-4 flex flex-col-reverse gap-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end",
className
)}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
@@ -13,7 +13,7 @@
<div <div
bind:this={ref} bind:this={ref}
data-slot="alert-dialog-header" data-slot="alert-dialog-header"
class={cn("grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]", className)} class={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
@@ -1,20 +0,0 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="alert-dialog-media"
class={cn("bg-muted mb-2 inline-flex size-10 items-center justify-center rounded-md sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-6", className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -12,6 +12,9 @@
<AlertDialogPrimitive.Overlay <AlertDialogPrimitive.Overlay
bind:ref bind:ref
data-slot="alert-dialog-overlay" data-slot="alert-dialog-overlay"
class={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 z-50", className)} class={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...restProps} {...restProps}
/> />
@@ -1,7 +1,9 @@
<script lang="ts"> <script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
let { ...restProps }: AlertDialogPrimitive.PortalProps = $props(); type $$Props = AlertDialogPrimitive.PortalProps;
</script> </script>
<AlertDialogPrimitive.Portal {...restProps} /> <AlertDialogPrimitive.Portal {...$$restProps}>
<slot />
</AlertDialogPrimitive.Portal>
@@ -12,6 +12,6 @@
<AlertDialogPrimitive.Title <AlertDialogPrimitive.Title
bind:ref bind:ref
data-slot="alert-dialog-title" data-slot="alert-dialog-title"
class={cn("text-base font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2", className)} class={cn("text-lg font-semibold", className)}
{...restProps} {...restProps}
/> />
@@ -1,7 +0,0 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
let { open = $bindable(false), ...restProps }: AlertDialogPrimitive.RootProps = $props();
</script>
<AlertDialogPrimitive.Root bind:open {...restProps} />
@@ -1,5 +1,4 @@
import Root from "./alert-dialog.svelte"; import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import Portal from "./alert-dialog-portal.svelte";
import Trigger from "./alert-dialog-trigger.svelte"; import Trigger from "./alert-dialog-trigger.svelte";
import Title from "./alert-dialog-title.svelte"; import Title from "./alert-dialog-title.svelte";
import Action from "./alert-dialog-action.svelte"; import Action from "./alert-dialog-action.svelte";
@@ -9,7 +8,9 @@ import Header from "./alert-dialog-header.svelte";
import Overlay from "./alert-dialog-overlay.svelte"; import Overlay from "./alert-dialog-overlay.svelte";
import Content from "./alert-dialog-content.svelte"; import Content from "./alert-dialog-content.svelte";
import Description from "./alert-dialog-description.svelte"; import Description from "./alert-dialog-description.svelte";
import Media from "./alert-dialog-media.svelte";
const Root = AlertDialogPrimitive.Root;
const Portal = AlertDialogPrimitive.Portal;
export { export {
Root, Root,
@@ -23,7 +24,6 @@ export {
Overlay, Overlay,
Content, Content,
Description, Description,
Media,
// //
Root as AlertDialog, Root as AlertDialog,
Title as AlertDialogTitle, Title as AlertDialogTitle,
@@ -36,5 +36,4 @@ export {
Overlay as AlertDialogOverlay, Overlay as AlertDialogOverlay,
Content as AlertDialogContent, Content as AlertDialogContent,
Description as AlertDialogDescription, Description as AlertDialogDescription,
Media as AlertDialogMedia,
}; };
@@ -1,20 +0,0 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="alert-action"
class={cn("absolute top-2 right-2", className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -14,7 +14,7 @@
bind:this={ref} bind:this={ref}
data-slot="alert-description" data-slot="alert-description"
class={cn( class={cn(
"text-muted-foreground text-sm text-balance md:text-pretty [&_p:not(:last-child)]:mb-4 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3", "text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
className className
)} )}
{...restProps} {...restProps}
@@ -13,10 +13,7 @@
<div <div
bind:this={ref} bind:this={ref}
data-slot="alert-title" data-slot="alert-title"
class={cn( class={cn("col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight", className)}
"font-medium group-has-[>svg]/alert:col-start-2 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3",
className
)}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
@@ -2,11 +2,12 @@
import { type VariantProps, tv } from "tailwind-variants"; import { type VariantProps, tv } from "tailwind-variants";
export const alertVariants = tv({ export const alertVariants = tv({
base: "grid gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4 group/alert relative w-full", base: "relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
variants: { variants: {
variant: { variant: {
default: "bg-card text-card-foreground", default: "bg-card text-card-foreground",
destructive: "text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current", destructive:
"text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current",
}, },
}, },
defaultVariants: { defaultVariants: {
@@ -35,9 +36,9 @@
<div <div
bind:this={ref} bind:this={ref}
data-slot="alert" data-slot="alert"
role="alert"
class={cn(alertVariants({ variant }), className)} class={cn(alertVariants({ variant }), className)}
{...restProps} {...restProps}
role="alert"
> >
{@render children?.()} {@render children?.()}
</div> </div>
@@ -1,17 +1,14 @@
import Root from "./alert.svelte"; import Root from "./alert.svelte";
import Description from "./alert-description.svelte"; import Description from "./alert-description.svelte";
import Title from "./alert-title.svelte"; import Title from "./alert-title.svelte";
import Action from "./alert-action.svelte";
export { alertVariants, type AlertVariant } from "./alert.svelte"; export { alertVariants, type AlertVariant } from "./alert.svelte";
export { export {
Root, Root,
Description, Description,
Title, Title,
Action,
// //
Root as Alert, Root as Alert,
Description as AlertDescription, Description as AlertDescription,
Title as AlertTitle, Title as AlertTitle,
Action as AlertAction,
}; };
@@ -4,25 +4,25 @@
import { type VariantProps, tv } from "tailwind-variants"; import { type VariantProps, tv } from "tailwind-variants";
export const buttonVariants = tv({ export const buttonVariants = tv({
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-lg border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 active:not-aria-[haspopup]:translate-y-px aria-invalid:ring-3 [&_svg:not([class*='size-'])]:size-4 group/button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
variants: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
outline: "border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground", destructive:
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", "bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white",
ghost: "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground", outline:
destructive: "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30", "bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border",
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline", link: "text-primary underline-offset-4 hover:underline",
}, },
size: { size: {
default: "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", default: "h-9 px-4 py-2 has-[>svg]:px-3",
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", icon: "size-9",
icon: "size-8", "icon-sm": "size-8",
"icon-xs": "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3", "icon-lg": "size-10",
"icon-sm": "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
"icon-lg": "size-9",
}, },
}, },
defaultVariants: { defaultVariants: {
@@ -12,7 +12,7 @@
<CalendarPrimitive.Cell <CalendarPrimitive.Cell
bind:ref bind:ref
class={cn( class={cn(
"relative size-(--cell-size) p-0 text-center text-sm focus-within:z-20 [&:first-child[data-selected]_[data-bits-day]]:rounded-s-(--cell-radius) [&:last-child[data-selected]_[data-bits-day]]:rounded-e-(--cell-radius)", "size-(--cell-size) relative p-0 text-center text-sm focus-within:z-20 [&:first-child[data-selected]_[data-bits-day]]:rounded-l-md [&:last-child[data-selected]_[data-bits-day]]:rounded-r-md",
className className
)} )}
{...restProps} {...restProps}
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { buttonVariants } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils.js";
import { Calendar as CalendarPrimitive } from "bits-ui"; import { Calendar as CalendarPrimitive } from "bits-ui";
@@ -12,17 +13,18 @@
<CalendarPrimitive.Day <CalendarPrimitive.Day
bind:ref bind:ref
class={cn( class={cn(
"flex size-(--cell-size) flex-col items-center justify-center gap-1 rounded-(--cell-radius) p-0 leading-none font-normal whitespace-nowrap select-none", buttonVariants({ variant: "ghost" }),
"[&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)", "size-(--cell-size) flex select-none flex-col items-center justify-center gap-1 whitespace-nowrap p-0 font-normal leading-none",
"not-data-selected:hover:bg-accent/50 not-data-selected:hover:text-accent-foreground",
"[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground [&[data-today][data-disabled]]:text-muted-foreground", "[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground [&[data-today][data-disabled]]:text-muted-foreground",
"data-[selected]:bg-primary data-[selected]:text-primary-foreground data-[selected]:hover:text-foreground", "data-[selected]:bg-primary dark:data-[selected]:hover:bg-accent/50 data-[selected]:text-primary-foreground",
// Outside months // Outside months
"[&[data-outside-month]:not([data-selected])]:text-muted-foreground [&[data-outside-month]:not([data-selected])]:hover:text-accent-foreground", "[&[data-outside-month]:not([data-selected])]:text-muted-foreground [&[data-outside-month]:not([data-selected])]:hover:text-accent-foreground",
// Disabled // Disabled
"data-[disabled]:text-muted-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", "data-[disabled]:text-muted-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
// Unavailable // Unavailable
"data-[unavailable]:text-muted-foreground data-[unavailable]:line-through", "data-[unavailable]:text-muted-foreground data-[unavailable]:line-through",
// hover
"dark:hover:text-accent-foreground",
// focus // focus
"focus:border-ring focus:ring-ring/50 focus:relative", "focus:border-ring focus:ring-ring/50 focus:relative",
// inner spans // inner spans
@@ -11,6 +11,6 @@
<CalendarPrimitive.Grid <CalendarPrimitive.Grid
bind:ref bind:ref
class={cn("flex w-full border-collapse flex-col", className)} class={cn("mt-4 flex w-full border-collapse flex-col gap-1", className)}
{...restProps} {...restProps}
/> />
@@ -12,7 +12,7 @@
<CalendarPrimitive.Header <CalendarPrimitive.Header
bind:ref bind:ref
class={cn( class={cn(
"flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium", "h-(--cell-size) flex w-full items-center justify-center gap-1.5 text-sm font-medium",
className className
)} )}
{...restProps} {...restProps}
@@ -14,15 +14,11 @@
<span <span
class={cn( class={cn(
"has-focus:border-ring border-input has-focus:ring-ring/50 relative flex rounded-md border shadow-xs has-focus:ring-[3px]", "has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative flex rounded-md border",
className className
)} )}
> >
<CalendarPrimitive.MonthSelect <CalendarPrimitive.MonthSelect bind:ref class="absolute inset-0 opacity-0" {...restProps}>
bind:ref
class="bg-background dark:bg-popover dark:text-popover-foreground absolute inset-0 opacity-0"
{...restProps}
>
{#snippet child({ props, monthItems, selectedMonthItem })} {#snippet child({ props, monthItems, selectedMonthItem })}
<select {...props} {value} {onchange}> <select {...props} {value} {onchange}>
{#each monthItems as monthItem (monthItem.value)} {#each monthItems as monthItem (monthItem.value)}
@@ -37,7 +33,7 @@
{/each} {/each}
</select> </select>
<span <span
class="[&>svg]:text-muted-foreground flex h-(--cell-size) items-center gap-1 rounded-md ps-2 pe-1 text-sm font-medium select-none [&>svg]:size-3.5" class="[&>svg]:text-muted-foreground flex h-8 select-none items-center gap-1 rounded-md pl-2 pr-1 text-sm font-medium [&>svg]:size-3.5"
aria-hidden="true" aria-hidden="true"
> >
{monthItems.find((item) => item.value === value)?.label || selectedMonthItem.label} {monthItems.find((item) => item.value === value)?.label || selectedMonthItem.label}
@@ -10,6 +10,6 @@
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props(); }: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
</script> </script>
<div {...restProps} bind:this={ref} class={cn("flex w-full flex-col gap-4", className)}> <div {...restProps} bind:this={ref} class={cn("flex flex-col", className)}>
{@render children?.()} {@render children?.()}
</div> </div>
@@ -23,14 +23,9 @@
bind:ref bind:ref
class={cn( class={cn(
buttonVariants({ variant }), buttonVariants({ variant }),
"size-(--cell-size) bg-transparent p-0 select-none disabled:opacity-50 rtl:rotate-180", "size-(--cell-size) select-none bg-transparent p-0 disabled:opacity-50 rtl:rotate-180",
className className
)} )}
children={children || Fallback}
{...restProps} {...restProps}
> />
{#if children}
{@render children?.()}
{:else}
{@render Fallback()}
{/if}
</CalendarPrimitive.NextButton>
@@ -23,14 +23,9 @@
bind:ref bind:ref
class={cn( class={cn(
buttonVariants({ variant }), buttonVariants({ variant }),
"size-(--cell-size) bg-transparent p-0 select-none disabled:opacity-50 rtl:rotate-180", "size-(--cell-size) select-none bg-transparent p-0 disabled:opacity-50 rtl:rotate-180",
className className
)} )}
children={children || Fallback}
{...restProps} {...restProps}
> />
{#if children}
{@render children?.()}
{:else}
{@render Fallback()}
{/if}
</CalendarPrimitive.PrevButton>
@@ -13,15 +13,11 @@
<span <span
class={cn( class={cn(
"has-focus:border-ring border-input has-focus:ring-ring/50 relative flex rounded-md border shadow-xs has-focus:ring-[3px]", "has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative flex rounded-md border",
className className
)} )}
> >
<CalendarPrimitive.YearSelect <CalendarPrimitive.YearSelect bind:ref class="absolute inset-0 opacity-0" {...restProps}>
bind:ref
class="dark:bg-popover dark:text-popover-foreground absolute inset-0 opacity-0"
{...restProps}
>
{#snippet child({ props, yearItems, selectedYearItem })} {#snippet child({ props, yearItems, selectedYearItem })}
<select {...props} {value}> <select {...props} {value}>
{#each yearItems as yearItem (yearItem.value)} {#each yearItems as yearItem (yearItem.value)}
@@ -36,7 +32,7 @@
{/each} {/each}
</select> </select>
<span <span
class="[&>svg]:text-muted-foreground flex h-(--cell-size) items-center gap-1 rounded-md ps-2 pe-1 text-sm font-medium select-none [&>svg]:size-3.5" class="[&>svg]:text-muted-foreground flex h-8 select-none items-center gap-1 rounded-md pl-2 pr-1 text-sm font-medium [&>svg]:size-3.5"
aria-hidden="true" aria-hidden="true"
> >
{yearItems.find((item) => item.value === value)?.label || selectedYearItem.label} {yearItems.find((item) => item.value === value)?.label || selectedYearItem.label}
@@ -50,7 +50,7 @@ get along, so we shut typescript up by casting `value` to `never`.
{weekdayFormat} {weekdayFormat}
{disableDaysOutsideMonth} {disableDaysOutsideMonth}
class={cn( class={cn(
"p-2 [--cell-radius:var(--radius-md)] [--cell-size:--spacing(7)] bg-background group/calendar in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent", "bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
className className
)} )}
{locale} {locale}
@@ -13,10 +13,7 @@
<div <div
bind:this={ref} bind:this={ref}
data-slot="card-action" data-slot="card-action"
class={cn( class={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
"cn-card-action col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
@@ -10,11 +10,6 @@
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props(); }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script> </script>
<div <div bind:this={ref} data-slot="card-content" class={cn("px-6", className)} {...restProps}>
bind:this={ref}
data-slot="card-content"
class={cn("px-4 group-data-[size=sm]/card:px-3", className)}
{...restProps}
>
{@render children?.()} {@render children?.()}
</div> </div>
@@ -13,7 +13,7 @@
<div <div
bind:this={ref} bind:this={ref}
data-slot="card-footer" data-slot="card-footer"
class={cn("bg-muted/50 rounded-b-xl border-t p-4 group-data-[size=sm]/card:p-3 flex items-center", className)} class={cn("[.border-t]:pt-6 flex items-center px-6", className)}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
@@ -14,7 +14,7 @@
bind:this={ref} bind:this={ref}
data-slot="card-header" data-slot="card-header"
class={cn( class={cn(
"gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]", "@container/card-header has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6 grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6",
className className
)} )}
{...restProps} {...restProps}
@@ -13,7 +13,7 @@
<div <div
bind:this={ref} bind:this={ref}
data-slot="card-title" data-slot="card-title"
class={cn("text-base leading-snug font-medium group-data-[size=sm]/card:text-sm", className)} class={cn("font-semibold leading-none", className)}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
@@ -6,16 +6,17 @@
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
children, children,
size = "default",
...restProps ...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { size?: "default" | "sm" } = $props(); }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script> </script>
<div <div
bind:this={ref} bind:this={ref}
data-slot="card" data-slot="card"
data-size={size} class={cn(
class={cn("ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-xl py-4 text-sm ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col", className)} "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { Checkbox as CheckboxPrimitive } from "bits-ui"; import { Checkbox as CheckboxPrimitive } from "bits-ui";
import CheckIcon from "@lucide/svelte/icons/check";
import MinusIcon from "@lucide/svelte/icons/minus";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js"; import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import CheckIcon from '@lucide/svelte/icons/check';
import MinusIcon from '@lucide/svelte/icons/minus';
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -17,7 +17,7 @@
bind:ref bind:ref
data-slot="checkbox" data-slot="checkbox"
class={cn( class={cn(
"border-input dark:bg-input/30 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex size-4 items-center justify-center rounded-[4px] border transition-colors group-has-disabled/field:opacity-50 focus-visible:ring-3 aria-invalid:ring-3 peer relative shrink-0 outline-none after:absolute after:-inset-x-3 after:-inset-y-2 disabled:cursor-not-allowed disabled:opacity-50", "border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive shadow-xs peer flex size-4 shrink-0 items-center justify-center rounded-[4px] border outline-none transition-shadow focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className className
)} )}
bind:checked bind:checked
@@ -25,14 +25,11 @@
{...restProps} {...restProps}
> >
{#snippet children({ checked, indeterminate })} {#snippet children({ checked, indeterminate })}
<div <div data-slot="checkbox-indicator" class="text-current transition-none">
data-slot="checkbox-indicator"
class="[&>svg]:size-3.5 grid place-content-center text-current transition-none"
>
{#if checked} {#if checked}
<CheckIcon /> <CheckIcon class="size-3.5" />
{:else if indeterminate} {:else if indeterminate}
<MinusIcon /> <MinusIcon class="size-3.5" />
{/if} {/if}
</div> </div>
{/snippet} {/snippet}
@@ -1,19 +1,17 @@
<script lang="ts"> <script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui"; import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import CheckIcon from "@lucide/svelte/icons/check";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js"; import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import type { Snippet } from "svelte"; import type { Snippet } from "svelte";
import CheckIcon from '@lucide/svelte/icons/check';
let { let {
ref = $bindable(null), ref = $bindable(null),
checked = $bindable(false), checked = $bindable(false),
indeterminate = $bindable(false), indeterminate = $bindable(false),
class: className, class: className,
inset,
children: childrenProp, children: childrenProp,
...restProps ...restProps
}: WithoutChildrenOrChild<ContextMenuPrimitive.CheckboxItemProps> & { }: WithoutChildrenOrChild<ContextMenuPrimitive.CheckboxItemProps> & {
inset?: boolean;
children?: Snippet; children?: Snippet;
} = $props(); } = $props();
</script> </script>
@@ -23,17 +21,16 @@
bind:checked bind:checked
bind:indeterminate bind:indeterminate
data-slot="context-menu-checkbox-item" data-slot="context-menu-checkbox-item"
data-inset={inset}
class={cn( class={cn(
"focus:bg-accent focus:text-accent-foreground gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", "data-highlighted:bg-accent data-highlighted:text-accent-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className className
)} )}
{...restProps} {...restProps}
> >
{#snippet children({ checked })} {#snippet children({ checked })}
<span class="absolute right-2 pointer-events-none"> <span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
{#if checked} {#if checked}
<CheckIcon /> <CheckIcon class="size-4" />
{/if} {/if}
</span> </span>
{@render childrenProp?.()} {@render childrenProp?.()}
@@ -1,9 +1,6 @@
<script lang="ts"> <script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui"; import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils.js";
import ContextMenuPortal from "./context-menu-portal.svelte";
import type { ComponentProps } from "svelte";
import type { WithoutChildrenOrChild } from "$lib/utils.js";
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -11,18 +8,18 @@
class: className, class: className,
...restProps ...restProps
}: ContextMenuPrimitive.ContentProps & { }: ContextMenuPrimitive.ContentProps & {
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof ContextMenuPortal>>; portalProps?: ContextMenuPrimitive.PortalProps;
} = $props(); } = $props();
</script> </script>
<ContextMenuPortal {...portalProps}> <ContextMenuPrimitive.Portal {...portalProps}>
<ContextMenuPrimitive.Content <ContextMenuPrimitive.Content
bind:ref bind:ref
data-slot="context-menu-content" data-slot="context-menu-content"
class={cn( class={cn(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-36 rounded-lg p-1 shadow-md ring-1 duration-100 z-50 overflow-x-hidden overflow-y-auto outline-none", "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-(--bits-context-menu-content-available-height) origin-(--bits-context-menu-content-transform-origin) z-50 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border p-1 shadow-md",
className className
)} )}
{...restProps} {...restProps}
/> />
</ContextMenuPortal> </ContextMenuPrimitive.Portal>
@@ -16,6 +16,6 @@
bind:ref bind:ref
data-slot="context-menu-group-heading" data-slot="context-menu-group-heading"
data-inset={inset} data-inset={inset}
class={cn("text-foreground px-2 py-1.5 text-sm font-medium data-inset:ps-8", className)} class={cn("text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", className)}
{...restProps} {...restProps}
/> />
@@ -20,7 +20,7 @@
data-inset={inset} data-inset={inset}
data-variant={variant} data-variant={variant}
class={cn( class={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive focus:*:[svg]:text-accent-foreground gap-1.5 rounded-md px-1.5 py-1 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 group/context-menu-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", "data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:data-highlighted:bg-destructive/10 dark:data-[variant=destructive]:data-highlighted:bg-destructive/20 data-[variant=destructive]:data-highlighted:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled]:pointer-events-none data-[inset]:pl-8 data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className className
)} )}
{...restProps} {...restProps}
@@ -17,7 +17,7 @@
bind:this={ref} bind:this={ref}
data-slot="context-menu-label" data-slot="context-menu-label"
data-inset={inset} data-inset={inset}
class={cn("text-muted-foreground px-1.5 py-1 text-xs font-medium data-inset:pl-7 data-inset:pl-8", className)} class={cn("text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", className)}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
@@ -1,7 +0,0 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
let { ...restProps }: ContextMenuPrimitive.PortalProps = $props();
</script>
<ContextMenuPrimitive.Portal {...restProps} />
@@ -1,33 +1,29 @@
<script lang="ts"> <script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui"; import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import CircleIcon from "@lucide/svelte/icons/circle";
import { cn, type WithoutChild } from "$lib/utils.js"; import { cn, type WithoutChild } from "$lib/utils.js";
import CheckIcon from '@lucide/svelte/icons/check';
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
inset,
children: childrenProp, children: childrenProp,
...restProps ...restProps
}: WithoutChild<ContextMenuPrimitive.RadioItemProps> & { }: WithoutChild<ContextMenuPrimitive.RadioItemProps> = $props();
inset?: boolean;
} = $props();
</script> </script>
<ContextMenuPrimitive.RadioItem <ContextMenuPrimitive.RadioItem
bind:ref bind:ref
data-slot="context-menu-radio-item" data-slot="context-menu-radio-item"
data-inset={inset}
class={cn( class={cn(
"focus:bg-accent focus:text-accent-foreground gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", "data-highlighted:bg-accent data-highlighted:text-accent-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className className
)} )}
{...restProps} {...restProps}
> >
{#snippet children({ checked })} {#snippet children({ checked })}
<span class="absolute right-2 pointer-events-none"> <span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
{#if checked} {#if checked}
<CheckIcon /> <CircleIcon class="size-2 fill-current" />
{/if} {/if}
</span> </span>
{@render childrenProp?.({ checked })} {@render childrenProp?.({ checked })}

Some files were not shown because too many files have changed in this diff Show More