chore: init
This commit is contained in:
18
src/lib/POIIcons.ts
Normal file
18
src/lib/POIIcons.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { BriefcaseMedicalIcon, CarIcon, ChefHatIcon, CroissantIcon, DrillIcon, FlameIcon, FuelIcon, HamburgerIcon, PackageIcon, SchoolIcon, SquareParkingIcon, StoreIcon } from "@lucide/svelte";
|
||||
import type { Component } from "svelte";
|
||||
|
||||
export const POIIcons: Record<string, Component> = {
|
||||
"amenity=school": SchoolIcon,
|
||||
"amenity=doctors": BriefcaseMedicalIcon,
|
||||
"amenity=parking": SquareParkingIcon,
|
||||
"shop=doityourself": DrillIcon,
|
||||
"shop=car": CarIcon,
|
||||
"amenity=fuel": FuelIcon,
|
||||
"shop=supermarket": StoreIcon,
|
||||
"amenity=parcel_locker": PackageIcon,
|
||||
"amenity=fire_station": FlameIcon,
|
||||
"shop=kiosk": StoreIcon,
|
||||
"amenity=restaurant": ChefHatIcon,
|
||||
"amenity=fast_food": HamburgerIcon,
|
||||
"shop=bakery": CroissantIcon
|
||||
};
|
||||
169
src/lib/components/lnv/AddVehicleDrawer.svelte
Normal file
169
src/lib/components/lnv/AddVehicleDrawer.svelte
Normal file
@@ -0,0 +1,169 @@
|
||||
<script lang="ts">
|
||||
import * as Drawer from "$lib/components/ui/drawer/index.js";
|
||||
import { BikeIcon, CarIcon, PlusCircleIcon, SaveIcon, TractorIcon, TruckIcon, XIcon } from "@lucide/svelte";
|
||||
import Button, { buttonVariants } from "../ui/button/button.svelte";
|
||||
import { DefaultVehicle, isValidFuel, selectVehicle, setVehicles, vehicles, type Vehicle, type VehicleType } from "$lib/vehicles/vehicles.svelte";
|
||||
import type { Snippet } from "svelte";
|
||||
import * as Select from "../ui/select";
|
||||
import Input from "../ui/input/input.svelte";
|
||||
import EvConnectorSelect from "./EVConnectorSelect.svelte";
|
||||
|
||||
let open = $state(false);
|
||||
|
||||
let { children }: { children: Snippet } = $props();
|
||||
|
||||
function getVehicleIcon(type: VehicleType) {
|
||||
switch (type) {
|
||||
case "car":
|
||||
return CarIcon;
|
||||
case "motor_scooter":
|
||||
return TractorIcon;
|
||||
case "bicycle":
|
||||
return BikeIcon;
|
||||
case "motorcycle":
|
||||
return BikeIcon;
|
||||
case "truck":
|
||||
return TruckIcon;
|
||||
default:
|
||||
return TractorIcon; // Default icon if no match
|
||||
}
|
||||
}
|
||||
|
||||
let vehicle: Vehicle = $state({
|
||||
name: "",
|
||||
type: "motor_scooter",
|
||||
legalMaxSpeed: 45,
|
||||
actualMaxSpeed: 45,
|
||||
emissionClass: "euro_5",
|
||||
fuelType: "diesel",
|
||||
preferredFuel: "Diesel"
|
||||
});
|
||||
</script>
|
||||
|
||||
<Drawer.Root bind:open={open}>
|
||||
<Drawer.Trigger class={buttonVariants({ variant: "secondary", class: "w-full p-5" })}>
|
||||
{@render children()}
|
||||
</Drawer.Trigger>
|
||||
<Drawer.Content>
|
||||
<Drawer.Header>
|
||||
<Drawer.Title>Add Vehicle</Drawer.Title>
|
||||
</Drawer.Header>
|
||||
<div class="p-4 pt-0 flex flex-col gap-2">
|
||||
<div class="flex gap-2">
|
||||
<Select.Root type="single" bind:value={vehicle.type}>
|
||||
<Select.Trigger class="w-[180px]">
|
||||
{@const Icon = getVehicleIcon(vehicle.type)}
|
||||
<Icon />
|
||||
{vehicle.type === "car" ? "Car" : vehicle.type === "motor_scooter" ? "Moped" : "?"}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="car">
|
||||
<CarIcon />
|
||||
Car
|
||||
</Select.Item>
|
||||
<Select.Item value="motor_scooter">
|
||||
<TractorIcon />
|
||||
Moped
|
||||
</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Vehicle Name"
|
||||
bind:value={vehicle.name}
|
||||
class="w-full"
|
||||
aria-label="Vehicle Name"
|
||||
aria-required="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-around mt-4">
|
||||
<span>Legal Speed</span>
|
||||
<span>/</span>
|
||||
<span>Actual Speed</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Legal Speed"
|
||||
bind:value={vehicle.legalMaxSpeed}
|
||||
class="w-full text-center"
|
||||
aria-label="Legal Max Speed"
|
||||
aria-required="true"
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Actual Speed"
|
||||
bind:value={vehicle.actualMaxSpeed}
|
||||
class="w-full text-center"
|
||||
aria-label="Actual Max Speed"
|
||||
aria-required="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-around mt-4">
|
||||
<span>Fuel Type</span>
|
||||
<span>/</span>
|
||||
<span>Preferred Fuel</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Select.Root type="single" bind:value={vehicle.fuelType}>
|
||||
<Select.Trigger class="w-full">
|
||||
{vehicle.fuelType === "diesel" ? "Diesel" : vehicle.fuelType === "petrol" ? "Petrol" : "Electric"}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="diesel">Diesel</Select.Item>
|
||||
<Select.Item value="petrol">Petrol</Select.Item>
|
||||
<Select.Item value="electric">Electric</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
|
||||
<Select.Root type="single" bind:value={vehicle.preferredFuel}>
|
||||
<Select.Trigger class="w-full">
|
||||
{vehicle.preferredFuel}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#if vehicle.fuelType === "diesel"}
|
||||
<Select.Item value="Diesel">Diesel</Select.Item>
|
||||
{:else if vehicle.fuelType === "petrol"}
|
||||
<Select.Item value="Super">Super</Select.Item>
|
||||
<Select.Item value="Super E10">Super E10</Select.Item>
|
||||
{:else if vehicle.fuelType === "electric"}
|
||||
<EvConnectorSelect />
|
||||
{/if}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
</div>
|
||||
<Drawer.Footer>
|
||||
<Button onclick={() => {
|
||||
open = false;
|
||||
if (vehicle.name.trim() === "") {
|
||||
alert("Please enter a vehicle name.");
|
||||
return;
|
||||
}
|
||||
if (vehicle.legalMaxSpeed <= 0 || vehicle.actualMaxSpeed <= 0) {
|
||||
alert("Please enter valid speeds.");
|
||||
return;
|
||||
}
|
||||
if(!isValidFuel(vehicle)) {
|
||||
alert("Please select a valid fuel type and preferred fuel.");
|
||||
return;
|
||||
}
|
||||
setVehicles([...vehicles, vehicle]);
|
||||
selectVehicle(vehicle);
|
||||
location.reload(); // TODO
|
||||
}}>
|
||||
<SaveIcon />
|
||||
Save
|
||||
</Button>
|
||||
<Button variant="secondary" onclick={() => {
|
||||
open = false;
|
||||
}}>
|
||||
<XIcon />
|
||||
Cancel
|
||||
</Button>
|
||||
</Drawer.Footer>
|
||||
</Drawer.Content>
|
||||
</Drawer.Root>
|
||||
10
src/lib/components/lnv/EVConnectorSelect.svelte
Normal file
10
src/lib/components/lnv/EVConnectorSelect.svelte
Normal file
@@ -0,0 +1,10 @@
|
||||
<script>
|
||||
import { EVConnectors } from "$lib/vehicles/vehicles.svelte";
|
||||
import * as Select from "../ui/select";
|
||||
</script>
|
||||
|
||||
{#each EVConnectors as connector}
|
||||
<Select.Item value={connector}>
|
||||
{connector}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
111
src/lib/components/lnv/LocationSelect.svelte
Normal file
111
src/lib/components/lnv/LocationSelect.svelte
Normal file
@@ -0,0 +1,111 @@
|
||||
<script lang="ts">
|
||||
import CheckIcon from "@lucide/svelte/icons/check";
|
||||
import ChevronsUpDownIcon from "@lucide/svelte/icons/chevrons-up-down";
|
||||
import { tick } from "svelte";
|
||||
import * as Command from "$lib/components/ui/command/index.js";
|
||||
import * as Popover from "$lib/components/ui/popover/index.js";
|
||||
import { Button } from "$lib/components/ui/button/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
const frameworks = [
|
||||
{
|
||||
value: "sveltekit",
|
||||
label: "SvelteKit",
|
||||
},
|
||||
{
|
||||
value: "next.js",
|
||||
label: "Next.js",
|
||||
},
|
||||
{
|
||||
value: "nuxt.js",
|
||||
label: "Nuxt.js",
|
||||
},
|
||||
{
|
||||
value: "remix",
|
||||
label: "Remix",
|
||||
},
|
||||
{
|
||||
value: "astro",
|
||||
label: "Astro",
|
||||
},
|
||||
];
|
||||
|
||||
let open = $state(false);
|
||||
let value = $state("");
|
||||
let triggerRef = $state<HTMLButtonElement>(null!);
|
||||
|
||||
const selectedValue = $derived(
|
||||
value === "location" ? "My Location" : value
|
||||
);
|
||||
|
||||
// We want to refocus the trigger button when the user selects
|
||||
// an item from the list so users can continue navigating the
|
||||
// rest of the form with the keyboard.
|
||||
function closeAndFocusTrigger() {
|
||||
open = false;
|
||||
tick().then(() => {
|
||||
triggerRef.focus();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<Popover.Root bind:open>
|
||||
<Popover.Trigger bind:ref={triggerRef}>
|
||||
{#snippet child({ props }: { props: Record<string, any> })}
|
||||
<Button
|
||||
variant="outline"
|
||||
class="justify-between"
|
||||
{...props}
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
>
|
||||
{selectedValue || "Select a location..."}
|
||||
<ChevronsUpDownIcon class="ml-2 size-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</Popover.Trigger>
|
||||
<Popover.Content class="w-[200px] p-0">
|
||||
<Command.Root>
|
||||
<Command.Input placeholder="Search..." />
|
||||
<Command.List>
|
||||
<Command.Empty>No location found.</Command.Empty>
|
||||
<Command.Group>
|
||||
<Command.Item
|
||||
value={"location"}
|
||||
onSelect={() => {
|
||||
value = "location";
|
||||
closeAndFocusTrigger();
|
||||
}}
|
||||
>
|
||||
<CheckIcon
|
||||
class={cn(
|
||||
"mr-2 size-4",
|
||||
value !== "location" && "text-transparent"
|
||||
)}
|
||||
/>
|
||||
My Location
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
<Command.Group>
|
||||
{#each frameworks as framework}
|
||||
<Command.Item
|
||||
value={framework.value}
|
||||
onSelect={() => {
|
||||
value = framework.value;
|
||||
closeAndFocusTrigger();
|
||||
}}
|
||||
>
|
||||
<CheckIcon
|
||||
class={cn(
|
||||
"mr-2 size-4",
|
||||
value !== framework.value && "text-transparent"
|
||||
)}
|
||||
/>
|
||||
{framework.label}
|
||||
</Command.Item>
|
||||
{/each}
|
||||
</Command.Group>
|
||||
</Command.List>
|
||||
</Command.Root>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
180
src/lib/components/lnv/Map.svelte
Normal file
180
src/lib/components/lnv/Map.svelte
Normal file
@@ -0,0 +1,180 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import {
|
||||
GeoJSONSource,
|
||||
GeolocateControl,
|
||||
Hash,
|
||||
LineLayer,
|
||||
MapLibre,
|
||||
Marker,
|
||||
Protocol,
|
||||
} from "svelte-maplibre-gl";
|
||||
import { view } from "./sidebar.svelte";
|
||||
import { geolocate, map, pin } from "./map.svelte";
|
||||
import {
|
||||
drawAllRoutes,
|
||||
fetchRoute,
|
||||
routing,
|
||||
} from "$lib/services/navigation/routing.svelte";
|
||||
import { createValhallaRequest } from "$lib/vehicles/ValhallaVehicles";
|
||||
import { ROUTING_SERVER } from "$lib/services/hosts";
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener("resize", map.updateMapPadding);
|
||||
map.updateMapPadding();
|
||||
});
|
||||
</script>
|
||||
|
||||
<Protocol
|
||||
scheme="tiles"
|
||||
loadFn={async (params) => {
|
||||
console.log(params.url);
|
||||
const url = params.url.replace("tiles://", "").replace("tiles.openfreemap.org/", "");
|
||||
const path = url.split("/")[0];
|
||||
if (path == "natural_earth") {
|
||||
const t = await fetch("https://tiles.openfreemap.org/" + url);
|
||||
if (t.status == 200) {
|
||||
const buffer = await t.arrayBuffer();
|
||||
return { data: buffer };
|
||||
} else {
|
||||
throw new Error(`Tile fetch error: ${t.statusText}`);
|
||||
}
|
||||
} else if (path == "planet") {
|
||||
const t = await fetch("https://tiles.openfreemap.org/" + url);
|
||||
if (t.status == 200) {
|
||||
const buffer = await t.arrayBuffer();
|
||||
return { data: buffer };
|
||||
} else {
|
||||
throw new Error(`Tile fetch error: ${t.statusText}`);
|
||||
}
|
||||
} else {
|
||||
throw new Error("Invalid tiles protocol path");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<MapLibre
|
||||
class="w-full h-full"
|
||||
style="/style.json"
|
||||
bind:map={map.value}
|
||||
padding={map.padding}
|
||||
onload={async () => {
|
||||
map.updateMapPadding();
|
||||
}}
|
||||
onclick={(e) => {
|
||||
if (view.current.type == "main" || view.current.type == "info") {
|
||||
pin.dropPin(e.lngLat.lat, e.lngLat.lng);
|
||||
pin.showInfo();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<!-- <Hash /> -->
|
||||
<GeolocateControl
|
||||
positionOptions={{
|
||||
enableHighAccuracy: true,
|
||||
}}
|
||||
trackUserLocation={true}
|
||||
autoTrigger={true}
|
||||
ongeolocate={(e: GeolocationPosition) => {
|
||||
const speed = Math.round((e.coords.speed || 0) * 3.6); // In km/h
|
||||
const accuracy = Math.round(e.coords.accuracy);
|
||||
geolocate.currentLocation = {
|
||||
lat: e.coords.latitude,
|
||||
lon: e.coords.longitude,
|
||||
};
|
||||
// $inspect(`Geolocation: ${e.coords.latitude}, ${e.coords.longitude} (Speed: ${speed} km/h, Accuracy: ${accuracy} m)`);
|
||||
}}
|
||||
/>
|
||||
{#if pin.isDropped}
|
||||
<Marker lnglat={{ lat: pin.lat, lng: pin.lng }} />
|
||||
{/if}
|
||||
|
||||
{#if routing.geojson.routePast}
|
||||
<GeoJSONSource id="route-past" data={routing.geojson.routePast}>
|
||||
<LineLayer
|
||||
id="route-past-border"
|
||||
source="route-past"
|
||||
layout={{ "line-join": "round", "line-cap": "round" }}
|
||||
paint={{
|
||||
"line-color": "#FFFFFF",
|
||||
"line-width": 13,
|
||||
}}
|
||||
></LineLayer>
|
||||
<LineLayer
|
||||
id="route-past"
|
||||
source="route-past"
|
||||
layout={{ "line-join": "round", "line-cap": "round" }}
|
||||
paint={{
|
||||
"line-color": "#acacac",
|
||||
"line-width": 8,
|
||||
}}
|
||||
></LineLayer>
|
||||
</GeoJSONSource>
|
||||
{/if}
|
||||
{#if routing.geojson.al0}
|
||||
<GeoJSONSource id="al0" data={routing.geojson.al0}>
|
||||
<LineLayer
|
||||
id="al0-border"
|
||||
source="al0"
|
||||
layout={{ "line-join": "round", "line-cap": "round" }}
|
||||
paint={{
|
||||
"line-color": "#FFFFFF",
|
||||
"line-width": 13,
|
||||
}}
|
||||
></LineLayer>
|
||||
<LineLayer
|
||||
id="al0"
|
||||
source="al0"
|
||||
layout={{ "line-join": "round", "line-cap": "round" }}
|
||||
paint={{
|
||||
"line-color": "#94aad4",
|
||||
"line-width": 8,
|
||||
}}
|
||||
></LineLayer>
|
||||
</GeoJSONSource>
|
||||
{/if}
|
||||
{#if routing.geojson.al1}
|
||||
<GeoJSONSource id="al1" data={routing.geojson.al1}>
|
||||
<LineLayer
|
||||
id="al1-border"
|
||||
source="al1"
|
||||
layout={{ "line-join": "round", "line-cap": "round" }}
|
||||
paint={{
|
||||
"line-color": "#FFFFFF",
|
||||
"line-width": 13,
|
||||
}}
|
||||
></LineLayer>
|
||||
<LineLayer
|
||||
id="al1"
|
||||
source="al1"
|
||||
layout={{ "line-join": "round", "line-cap": "round" }}
|
||||
paint={{
|
||||
"line-color": "#94aad4",
|
||||
"line-width": 8,
|
||||
}}
|
||||
></LineLayer>
|
||||
</GeoJSONSource>
|
||||
{/if}
|
||||
{#if routing.geojson.route}
|
||||
<GeoJSONSource id="route" data={routing.geojson.route}>
|
||||
<LineLayer
|
||||
id="route-border"
|
||||
source="route"
|
||||
layout={{ "line-join": "round", "line-cap": "round" }}
|
||||
paint={{
|
||||
"line-color": "#FFFFFF",
|
||||
"line-width": 13,
|
||||
}}
|
||||
></LineLayer>
|
||||
<LineLayer
|
||||
id="route"
|
||||
source="route"
|
||||
layout={{ "line-join": "round", "line-cap": "round" }}
|
||||
paint={{
|
||||
"line-color": "#3478f6",
|
||||
"line-width": 8,
|
||||
}}
|
||||
></LineLayer>
|
||||
</GeoJSONSource>
|
||||
{/if}
|
||||
</MapLibre>
|
||||
0
src/lib/components/lnv/Post.svelte
Normal file
0
src/lib/components/lnv/Post.svelte
Normal file
17
src/lib/components/lnv/RequiresCapability.svelte
Normal file
17
src/lib/components/lnv/RequiresCapability.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { hasCapability, type Capabilities } from "$lib/services/lnv";
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
let { capability, children }: {
|
||||
capability: Capabilities[number];
|
||||
children: Snippet;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
{#await hasCapability(capability) then has}
|
||||
{#if has}
|
||||
{@render children()}
|
||||
{/if}
|
||||
{:catch error}
|
||||
<!-- user is likely offline -->
|
||||
{/await}
|
||||
204
src/lib/components/lnv/Sidebar.svelte
Normal file
204
src/lib/components/lnv/Sidebar.svelte
Normal file
@@ -0,0 +1,204 @@
|
||||
<script lang="ts">
|
||||
import { onMount, type Component } from "svelte";
|
||||
import InvalidSidebar from "./sidebar/InvalidSidebar.svelte";
|
||||
import { searchbar, view } from "./sidebar.svelte";
|
||||
import MainSidebar from "./sidebar/MainSidebar.svelte";
|
||||
import InfoSidebar from "./sidebar/InfoSidebar.svelte";
|
||||
import RouteSidebar from "./sidebar/RouteSidebar.svelte";
|
||||
import { map } from "./map.svelte";
|
||||
import TripSidebar from "./sidebar/TripSidebar.svelte";
|
||||
import Input from "../ui/input/input.svelte";
|
||||
import { HomeIcon, SettingsIcon, UserIcon } from "@lucide/svelte";
|
||||
import Button from "../ui/button/button.svelte";
|
||||
import { search, type Feature } from "$lib/services/Search";
|
||||
import SearchSidebar from "./sidebar/SearchSidebar.svelte";
|
||||
|
||||
const views: {[key: string]: Component<any>} = {
|
||||
main: MainSidebar,
|
||||
info: InfoSidebar,
|
||||
route: RouteSidebar,
|
||||
trip: TripSidebar,
|
||||
search: SearchSidebar
|
||||
};
|
||||
|
||||
let isDragging = false;
|
||||
let startY = 0;
|
||||
let startHeight = 0;
|
||||
let sidebarHeight = $state(200);
|
||||
|
||||
let CurrentView = $derived(views[view.current.type] || InvalidSidebar);
|
||||
|
||||
function debounce<T>(getter: () => T, delay: number): () => T | undefined {
|
||||
let value = $state<T>();
|
||||
let timer: NodeJS.Timeout;
|
||||
$effect(() => {
|
||||
const newValue = getter(); // read here to subscribe to it
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => value = newValue, delay);
|
||||
return () => clearTimeout(timer);
|
||||
});
|
||||
return () => value;
|
||||
}
|
||||
|
||||
let loading = $state(false);
|
||||
|
||||
let searchText = $derived.by(debounce(() => searchbar.text, 300));
|
||||
let searchResults: Feature[] = $state([]);
|
||||
|
||||
$effect(() => {
|
||||
if(!searchText) {
|
||||
searchResults = [];
|
||||
if(view.current.type == "search") view.switch("main");
|
||||
return;
|
||||
}
|
||||
if (searchText.length > 0) {
|
||||
loading = true;
|
||||
search(searchText, 0, 0).then(results => {
|
||||
searchResults = results;
|
||||
loading = false;
|
||||
view.switch("search", {
|
||||
results: searchResults,
|
||||
query: searchText
|
||||
})
|
||||
});
|
||||
} else {
|
||||
searchResults = [];
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div id="floating-search">
|
||||
<Input class="h-10"
|
||||
placeholder="Search..." bind:value={searchbar.text} />
|
||||
</div>
|
||||
<div id="sidebar" style={window.innerWidth < 768 ? `height: ${sidebarHeight}px` : ""}>
|
||||
{#if window.innerWidth < 768}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_interactive_supports_focus -->
|
||||
<div role="button" id="grabber" style="height: 10px; cursor: grab; display: flex; justify-content: center; align-items: center; touch-action: none;" ontouchstart={(e) => {
|
||||
isDragging = true;
|
||||
startY = e.touches[0].clientY;
|
||||
startHeight = sidebarHeight;
|
||||
}} ontouchmove={(e) => {
|
||||
if(!isDragging) return;
|
||||
e.preventDefault();
|
||||
const deltaY = e.touches[0].clientY - startY;
|
||||
let newHeight = Math.max(100, startHeight - deltaY);
|
||||
|
||||
const snapPoint = 200;
|
||||
const snapThreshold = 20;
|
||||
if (Math.abs(newHeight - snapPoint) < snapThreshold) {
|
||||
newHeight = snapPoint;
|
||||
}
|
||||
sidebarHeight = newHeight;
|
||||
|
||||
map.updateMapPadding();
|
||||
}} ontouchend={() => {
|
||||
if(!isDragging) return;
|
||||
isDragging = false;
|
||||
}}>
|
||||
<div style="height: 8px; background-color: #acacac; width: 40%; border-radius: 15px;"></div>
|
||||
</div>
|
||||
{/if}
|
||||
<CurrentView {...view.current.props}></CurrentView>
|
||||
</div>
|
||||
<div id="navigation">
|
||||
<button>
|
||||
<HomeIcon />
|
||||
</button>
|
||||
<button>
|
||||
<UserIcon />
|
||||
</button>
|
||||
<button>
|
||||
<SettingsIcon />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#sidebar {
|
||||
background-color: hsla(0, 0%, 5%, 0.6);
|
||||
backdrop-filter: blur(5px);
|
||||
color: white;
|
||||
padding: 15px;
|
||||
width: calc(25% - 20px);
|
||||
max-width: calc(25% - 20px);
|
||||
overflow-y: scroll;
|
||||
height: calc(100vh - 20px - 40px - 10px - 50px);
|
||||
max-height: calc(100vh - 20px);
|
||||
|
||||
position: fixed;
|
||||
top: calc(40px + 10px);
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
margin: 10px;
|
||||
border-radius: 15px;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
#floating-search {
|
||||
position: fixed;
|
||||
margin: 10px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 20;
|
||||
width: calc(25% - 20px);
|
||||
background-color: hsla(0, 0%, 5%, 0.6);
|
||||
backdrop-filter: blur(5px);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
#navigation {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 50;
|
||||
background-color: hsla(0, 0%, 5%, 0.9);
|
||||
backdrop-filter: blur(5px);
|
||||
margin-bottom: 0;
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
width: calc(25% - 20px);
|
||||
/* border-top-left-radius: 15px;
|
||||
border-top-right-radius: 15px; */
|
||||
height: 50px;
|
||||
border-bottom-left-radius: 15px;
|
||||
border-bottom-right-radius: 15px;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#sidebar {
|
||||
position: fixed;
|
||||
top: unset;
|
||||
bottom: 50px;
|
||||
left: 0;
|
||||
/* min-width: calc(100% - 20px);
|
||||
max-width: calc(100% - 20px); */
|
||||
min-width: calc(100%);
|
||||
max-width: calc(100%);
|
||||
width: calc(100% - 20px);
|
||||
height: 200px;
|
||||
margin: unset;
|
||||
/* margin-left: 10px;
|
||||
margin-right: 10px; */
|
||||
border-radius: 0;
|
||||
border-top-left-radius: 15px;
|
||||
border-top-right-radius: 15px;
|
||||
padding-top: 5px; /* for the grabber */
|
||||
}
|
||||
|
||||
#floating-search {
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
|
||||
#navigation {
|
||||
margin: 0;
|
||||
width: calc(100%);
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
57
src/lib/components/lnv/VehicleSelector.svelte
Normal file
57
src/lib/components/lnv/VehicleSelector.svelte
Normal file
@@ -0,0 +1,57 @@
|
||||
<script lang="ts">
|
||||
import * as Drawer from "$lib/components/ui/drawer/index.js";
|
||||
import { BikeIcon, CarIcon, PlusCircleIcon, TractorIcon, TruckIcon } from "@lucide/svelte";
|
||||
import Button, { buttonVariants } from "../ui/button/button.svelte";
|
||||
import { DefaultVehicle, selectedVehicle, selectVehicle, vehicles, type VehicleType } from "$lib/vehicles/vehicles.svelte";
|
||||
import AddVehicleDrawer from "./AddVehicleDrawer.svelte";
|
||||
|
||||
let open = $state(false);
|
||||
|
||||
function getVehicleIcon(type: VehicleType) {
|
||||
switch (type) {
|
||||
case "car":
|
||||
return CarIcon;
|
||||
case "motor_scooter":
|
||||
return TractorIcon;
|
||||
case "bicycle":
|
||||
return BikeIcon;
|
||||
case "motorcycle":
|
||||
return BikeIcon;
|
||||
case "truck":
|
||||
return TruckIcon;
|
||||
default:
|
||||
return TractorIcon; // Default icon if no match
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Drawer.Root bind:open={open}>
|
||||
<Drawer.Trigger class={buttonVariants({ variant: "secondary", class: "w-full p-5" })}>
|
||||
{@const vehicle = selectedVehicle() ?? DefaultVehicle}
|
||||
{@const Icon = getVehicleIcon(vehicle.type)}
|
||||
<Icon />
|
||||
{vehicle.name}
|
||||
</Drawer.Trigger>
|
||||
<Drawer.Content>
|
||||
<Drawer.Header>
|
||||
<Drawer.Title>Vehicle Selector</Drawer.Title>
|
||||
<Drawer.Description>Select your vehicle to customize routing just for you.</Drawer.Description>
|
||||
</Drawer.Header>
|
||||
<div class="p-4 pt-0 flex flex-col gap-2">
|
||||
{#each vehicles as vehicle}
|
||||
<Button variant={selectedVehicle() === vehicle ? "default" : "secondary"} class="w-full p-5" onclick={() => {selectVehicle(vehicle); open = false;}}>
|
||||
{@const Icon = getVehicleIcon(vehicle.type)}
|
||||
<Icon />
|
||||
{vehicle.name}
|
||||
</Button>
|
||||
{/each}
|
||||
|
||||
<AddVehicleDrawer>
|
||||
<Button variant="secondary" class="w-full p-5">
|
||||
<PlusCircleIcon />
|
||||
Add Vehicle
|
||||
</Button>
|
||||
</AddVehicleDrawer>
|
||||
</div>
|
||||
</Drawer.Content>
|
||||
</Drawer.Root>
|
||||
41
src/lib/components/lnv/info/FuelStation.svelte
Normal file
41
src/lib/components/lnv/info/FuelStation.svelte
Normal file
@@ -0,0 +1,41 @@
|
||||
<script>
|
||||
import Badge from "$lib/components/ui/badge/badge.svelte";
|
||||
import { getStations } from "$lib/services/MTSK";
|
||||
|
||||
let { tags, lat, lng } = $props();
|
||||
</script>
|
||||
|
||||
<h3 class="text-lg font-bold mt-2">Fuel Types</h3>
|
||||
<ul class="flex gap-2 flex-wrap">
|
||||
{#each Object.entries(tags).filter(([key]) => key.startsWith("fuel:")) as [key, tag]}
|
||||
<!-- <li>{key.replace("fuel:", "")}: {tag}</li> -->
|
||||
<Badge>
|
||||
{key.replace("fuel:", "")}
|
||||
{#if tag !== "yes"}
|
||||
({tag})
|
||||
{/if}
|
||||
</Badge>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<h3 class="text-lg font-bold mt-2">Prices</h3>
|
||||
{#await getStations(lat, lng)}
|
||||
<p>Loading fuel prices...</p>
|
||||
{:then stations}
|
||||
{#if stations.stations.length > 0}
|
||||
{@const station = stations.stations[0]}
|
||||
{#if station.diesel}
|
||||
<p>Diesel: {station.diesel}</p>
|
||||
{/if}
|
||||
{#if station.e10}
|
||||
<p>E10: {station.e10}</p>
|
||||
{/if}
|
||||
{#if station.e5}
|
||||
<p>E5: {station.e5}</p>
|
||||
{/if}
|
||||
{:else}
|
||||
<p>No fuel prices available.</p>
|
||||
{/if}
|
||||
{:catch err}
|
||||
<p>Error loading fuel prices: {err.message}</p>
|
||||
{/await}
|
||||
41
src/lib/components/lnv/info/MapAI.svelte
Normal file
41
src/lib/components/lnv/info/MapAI.svelte
Normal file
@@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
import Input from "$lib/components/ui/input/input.svelte";
|
||||
import { LNV_SERVER } from "$lib/services/hosts";
|
||||
import { ai } from "$lib/services/lnv";
|
||||
import { SparklesIcon } from "@lucide/svelte";
|
||||
|
||||
let { lat, lon } = $props();
|
||||
let question = $state("");
|
||||
|
||||
function getText(res: string) {
|
||||
console.log("Response from MapAI:", res);
|
||||
const chunks = res.split("\n");
|
||||
let text = "";
|
||||
for (const chunk of chunks) {
|
||||
if(chunk.startsWith("0:")) {
|
||||
text += JSON.parse(chunk.substring(2).trim());
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex gap-2 mt-2 p-2 border-border border-solid border-2 rounded-lg">
|
||||
<SparklesIcon />
|
||||
<div class="flex gap-2 flex-col w-full">
|
||||
{#await ai(question, { lat, lon })}
|
||||
<p>Loading...</p>
|
||||
{:then data}
|
||||
{@const text = getText(data)}
|
||||
<p>{text}</p>
|
||||
{:catch error}
|
||||
<p>Error: {error.message}</p>
|
||||
{/await}
|
||||
<Input
|
||||
type="text"
|
||||
value={""}
|
||||
placeholder="Ask a question about this place..." onchange={(e) => {
|
||||
question = (e.target! as any).value;
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
27
src/lib/components/lnv/info/OpeningHours.svelte
Normal file
27
src/lib/components/lnv/info/OpeningHours.svelte
Normal file
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import Badge from "$lib/components/ui/badge/badge.svelte";
|
||||
import opening_hours from "opening_hours";
|
||||
|
||||
let { hours, lat, lon }: { hours: string, lat: number, lon: number } = $props();
|
||||
|
||||
const oh = $derived.by(() => {
|
||||
return new opening_hours(hours, {
|
||||
lat, lon, address: {
|
||||
country_code: "de", // Default to Germany, can be overridden if needed
|
||||
state: "NRW", // Default to North Rhine-Westphalia, can be overridden if needed
|
||||
}
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<h3 class="text-lg font-bold mt-2">
|
||||
Opening Hours
|
||||
{#if oh.getState()}
|
||||
<Badge>Open</Badge>
|
||||
{:else}
|
||||
<Badge variant="destructive">Closed</Badge>
|
||||
{/if}
|
||||
</h3>
|
||||
|
||||
<p>{hours}</p>
|
||||
<!-- todo -->
|
||||
46
src/lib/components/lnv/info/Reviews.svelte
Normal file
46
src/lib/components/lnv/info/Reviews.svelte
Normal file
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
import * as Avatar from "$lib/components/ui/avatar";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { getReviews, postReview } from "$lib/services/lnv";
|
||||
import Stars from "./Stars.svelte";
|
||||
|
||||
let { lat, lng }: { lat: number, lng: number } = $props();
|
||||
</script>
|
||||
|
||||
<h3 class="text-lg font-bold mt-2">Reviews</h3>
|
||||
{#await getReviews({lat, lon: lng}) then reviews}
|
||||
{#if reviews.length > 0}
|
||||
<ul class="list-disc pl-5">
|
||||
{#each reviews as review}
|
||||
<li class="flex justify-center gap-2 mb-2 flex-col">
|
||||
<div class="flex items-center gap-2">
|
||||
<Avatar.Root>
|
||||
<!-- <Avatar.Image src="https://github.com/shadcn.png" alt="@shadcn" /> -->
|
||||
<Avatar.Fallback>{review.username}</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<Stars rating={review.rating} />
|
||||
</div>
|
||||
<span>{review.comment}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<p>No reviews available.</p>
|
||||
{/if}
|
||||
<Button variant="secondary" onclick={async () => {
|
||||
const rating = prompt("Enter your rating (1-5):");
|
||||
const comment = prompt("Enter your review comment:");
|
||||
if (rating && comment) {
|
||||
console.log(`Rating: ${rating}, Comment: ${comment}`);
|
||||
await postReview({ lat, lon: lng }, {
|
||||
rating: parseInt(rating, 10),
|
||||
comment
|
||||
})
|
||||
alert("Thank you for your review!");
|
||||
} else {
|
||||
alert("Review submission cancelled.");
|
||||
}
|
||||
}} disabled>Write a review</Button><br>
|
||||
{:catch error}
|
||||
<p>Error loading reviews: {error.message}</p>
|
||||
{/await}
|
||||
43
src/lib/components/lnv/info/Stars.svelte
Normal file
43
src/lib/components/lnv/info/Stars.svelte
Normal file
@@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { StarIcon } from "@lucide/svelte";
|
||||
|
||||
let { rating }: { rating: number } = $props();
|
||||
</script>
|
||||
|
||||
{#if rating == 0}
|
||||
<StarIcon />
|
||||
<StarIcon />
|
||||
<StarIcon />
|
||||
<StarIcon />
|
||||
<StarIcon />
|
||||
{:else if rating == 1}
|
||||
<StarIcon class="fill-white" />
|
||||
<StarIcon />
|
||||
<StarIcon />
|
||||
<StarIcon />
|
||||
<StarIcon />
|
||||
{:else if rating == 2}
|
||||
<StarIcon class="fill-white" />
|
||||
<StarIcon class="fill-white" />
|
||||
<StarIcon />
|
||||
<StarIcon />
|
||||
<StarIcon />
|
||||
{:else if rating == 3}
|
||||
<StarIcon class="fill-white" />
|
||||
<StarIcon class="fill-white" />
|
||||
<StarIcon class="fill-white" />
|
||||
<StarIcon />
|
||||
<StarIcon />
|
||||
{:else if rating == 4}
|
||||
<StarIcon class="fill-white" />
|
||||
<StarIcon class="fill-white" />
|
||||
<StarIcon class="fill-white" />
|
||||
<StarIcon class="fill-white" />
|
||||
<StarIcon />
|
||||
{:else if rating == 5}
|
||||
<StarIcon class="fill-white" />
|
||||
<StarIcon class="fill-white" />
|
||||
<StarIcon class="fill-white" />
|
||||
<StarIcon class="fill-white" />
|
||||
<StarIcon class="fill-white" />
|
||||
{/if}
|
||||
76
src/lib/components/lnv/map.svelte.ts
Normal file
76
src/lib/components/lnv/map.svelte.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { reverseGeocode } from "$lib/services/Search";
|
||||
import { view } from "./sidebar.svelte";
|
||||
|
||||
export const geolocate = $state({
|
||||
currentLocation: null as WorldLocation | null,
|
||||
})
|
||||
|
||||
export const map = $state({
|
||||
value: undefined as maplibregl.Map | undefined,
|
||||
updateMapPadding: () => {
|
||||
if(document.querySelector<HTMLDivElement>("#sidebar") == null) {
|
||||
map._setPadding({
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0
|
||||
});
|
||||
return;
|
||||
}
|
||||
console.log("Updating map padding");
|
||||
if (window.innerWidth < 768) {
|
||||
const calculatedSidebarHeight = document.querySelector<HTMLDivElement>("#sidebar")!.getBoundingClientRect().height;
|
||||
map._setPadding({
|
||||
top: 50,
|
||||
right: 0,
|
||||
bottom: calculatedSidebarHeight + 50,
|
||||
left: 0
|
||||
});
|
||||
return;
|
||||
}
|
||||
const calculatedSidebarWidth = document.querySelector<HTMLDivElement>("#sidebar")!.getBoundingClientRect().width;
|
||||
map._setPadding({
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: calculatedSidebarWidth,
|
||||
});
|
||||
},
|
||||
padding: {
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0
|
||||
},
|
||||
_setPadding: (_padding: { top: number, right: number, bottom: number, left: number }) => {
|
||||
map.padding = _padding;
|
||||
if (map.value) {
|
||||
map.value.setPadding(map.padding);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const pin = $state({
|
||||
isDropped: false,
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
dropPin: (lat: number, lng: number) => {
|
||||
pin.isDropped = true;
|
||||
pin.lat = lat;
|
||||
pin.lng = lng;
|
||||
},
|
||||
liftPin: () => {
|
||||
pin.isDropped = false;
|
||||
pin.lat = 0;
|
||||
pin.lng = 0;
|
||||
},
|
||||
showInfo: async () => {
|
||||
if(!pin.isDropped) return;
|
||||
// const res = await reverseGeocode({ lat: pin.lat, lon: pin.lng });
|
||||
// if(res.length > 0) {
|
||||
// const feature = res[0];
|
||||
// view.switch("info", { feature });
|
||||
// }
|
||||
view.switch("info", { lat: pin.lat, lng: pin.lng });
|
||||
}
|
||||
})
|
||||
26
src/lib/components/lnv/sidebar.svelte.ts
Normal file
26
src/lib/components/lnv/sidebar.svelte.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export type View = {
|
||||
type: string;
|
||||
props?: Record<string, any>;
|
||||
}
|
||||
|
||||
export const view = $state({
|
||||
current: { type: "main" } as View,
|
||||
history: [] as View[],
|
||||
back: () => {
|
||||
if (view.history.length > 0) {
|
||||
view.current = view.history.pop()!;
|
||||
} else {
|
||||
view.current = { type: "main" } as View; // Reset to main view if history is empty
|
||||
}
|
||||
},
|
||||
switch: (to: string, props?: Record<string, any>) => {
|
||||
if (view.current.type !== to) {
|
||||
view.history.push(view.current);
|
||||
}
|
||||
view.current = { type: to, props } as View;
|
||||
}
|
||||
});
|
||||
|
||||
export const searchbar = $state({
|
||||
text: ""
|
||||
})
|
||||
196
src/lib/components/lnv/sidebar/InfoSidebar.svelte
Normal file
196
src/lib/components/lnv/sidebar/InfoSidebar.svelte
Normal file
@@ -0,0 +1,196 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { POIIcons } from "$lib/POIIcons";
|
||||
import { OVERPASS_SERVER } from "$lib/services/hosts";
|
||||
import { BriefcaseIcon, EllipsisIcon, GlobeIcon, HomeIcon, MailIcon, PhoneIcon, RouteIcon } from "@lucide/svelte";
|
||||
import { pin } from "../map.svelte";
|
||||
import SidebarHeader from "./SidebarHeader.svelte";
|
||||
import { fetchPOI, type OverpassElement } from "$lib/services/Overpass";
|
||||
import OpeningHours from "../info/OpeningHours.svelte";
|
||||
import Badge from "$lib/components/ui/badge/badge.svelte";
|
||||
import FuelStation from "../info/FuelStation.svelte";
|
||||
import { view } from "../sidebar.svelte";
|
||||
import * as Popover from "$lib/components/ui/popover";
|
||||
import Reviews from "../info/Reviews.svelte";
|
||||
import MapAi from "../info/MapAI.svelte";
|
||||
import { hasCapability } from "$lib/services/lnv";
|
||||
import RequiresCapability from "../RequiresCapability.svelte";
|
||||
|
||||
// let { feature }: { feature: Feature } = $props();
|
||||
|
||||
// let Icon = $derived(POIIcons[feature.properties.osm_key + "=" + feature.properties.osm_value]);
|
||||
|
||||
let { lat, lng }: { lat: number, lng: number } = $props();
|
||||
|
||||
function getIcon(tags: Record<string, string>): typeof POIIcons[keyof typeof POIIcons] | null {
|
||||
const key = Object.keys(tags).find(k => k.startsWith("amenity") || k.startsWith("shop"));
|
||||
if (key && POIIcons[key + "=" + tags[key]]) {
|
||||
return POIIcons[key + "=" + tags[key]];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getDistance(aLat: number, aLon: number, lat: number, lon: number): number {
|
||||
const R = 6371e3; // Earth radius in meters
|
||||
const φ1 = lat * Math.PI / 180;
|
||||
const φ2 = aLat * Math.PI / 180;
|
||||
const Δφ = (aLat - lat) * Math.PI / 180;
|
||||
const Δλ = (aLon - lon) * Math.PI / 180;
|
||||
|
||||
const a = Math.sin(Δφ/2)**2 + Math.cos(φ1)*Math.cos(φ2)*Math.sin(Δλ/2)**2;
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
|
||||
return R * c;
|
||||
}
|
||||
|
||||
function sortByDistance(elements: OverpassElement[], lat: number, lng: number): OverpassElement[] {
|
||||
return elements.sort((a: OverpassElement, b: OverpassElement) => {
|
||||
const aLoc = a.center || a;
|
||||
const bLoc = b.center || b;
|
||||
return getDistance(aLoc.lat!, aLoc.lon!, lat, lng) - getDistance(bLoc.lat!, bLoc.lon!, lat, lng);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{#await fetchPOI(lat, lng, 20)}
|
||||
<SidebarHeader onback={() => {
|
||||
pin.liftPin();
|
||||
}}>
|
||||
Dropped Pin
|
||||
</SidebarHeader>
|
||||
<p>Loading...</p>
|
||||
{:then res}
|
||||
{#if res.elements.length === 0}
|
||||
<SidebarHeader onback={() => {
|
||||
pin.liftPin();
|
||||
}}>
|
||||
Dropped Pin
|
||||
</SidebarHeader>
|
||||
<span style="color: #acacac;">© OpenStreetMap</span>
|
||||
<pre>{JSON.stringify(res, null, 2)}</pre>
|
||||
{:else}
|
||||
{@const elements = sortByDistance(res.elements, lat, lng)}
|
||||
{@const tags = elements[0].tags}
|
||||
{@const firstElement = elements[0]}
|
||||
{@const ellat = firstElement.center?.lat || firstElement.lat!}
|
||||
{@const ellng = firstElement.center?.lon || firstElement.lon!}
|
||||
|
||||
<SidebarHeader onback={() => {
|
||||
pin.liftPin();
|
||||
}}>
|
||||
{#if getIcon(tags)}
|
||||
{@const Icon = getIcon(tags)}
|
||||
<Icon />
|
||||
{/if}
|
||||
{tags.name || (tags["addr:street"] ? (tags["addr:street"] + " " + tags["addr:housenumber"]) : "")}
|
||||
</SidebarHeader>
|
||||
<div id="actions">
|
||||
<Button onclick={() => {
|
||||
view.switch("route", {
|
||||
to: lat + "," + lng,
|
||||
})
|
||||
}}>
|
||||
<RouteIcon />
|
||||
Route
|
||||
</Button>
|
||||
{#if tags.email || tags["contact:email"]}
|
||||
<Button variant="secondary" href={`mailto:${tags.email || tags["contact:email"]}`} target="_blank">
|
||||
<MailIcon />
|
||||
Email
|
||||
</Button>
|
||||
{/if}
|
||||
{#if tags.website || tags["contact:website"]}
|
||||
<Button variant="secondary" href={tags.website || tags["contact:website"]} target="_blank">
|
||||
<GlobeIcon />
|
||||
Website
|
||||
</Button>
|
||||
{/if}
|
||||
{#if tags.phone || tags["contact:phone"]}
|
||||
<Button variant="secondary" href={`tel:${tags.phone || tags["contact:phone"]}`} target="_blank">
|
||||
<PhoneIcon />
|
||||
Call
|
||||
</Button>
|
||||
{/if}
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
<Button variant="secondary">
|
||||
<EllipsisIcon />
|
||||
More
|
||||
</Button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content>
|
||||
<div class="flex flex-col gap-2">
|
||||
<Button variant="outline" onclick={() => {
|
||||
localStorage.setItem("saved.home", JSON.stringify({ lat, lon: lng }));
|
||||
}}>
|
||||
<HomeIcon />
|
||||
Set as Home
|
||||
</Button>
|
||||
<Button variant="outline" onclick={() => {
|
||||
localStorage.setItem("saved.work", JSON.stringify({ lat, lon: lng }));
|
||||
}}>
|
||||
<BriefcaseIcon />
|
||||
Set as Work
|
||||
</Button>
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</div>
|
||||
|
||||
<RequiresCapability capability="ai">
|
||||
<MapAi lat={ellat} lon={ellng} />
|
||||
</RequiresCapability>
|
||||
|
||||
<!--
|
||||
"addr:city": "Hagen",
|
||||
"addr:housenumber": "12",
|
||||
"addr:postcode": "58135",
|
||||
"addr:street": -->
|
||||
<p class="mt-2">{tags["addr:street"]} {tags["addr:housenumber"]}</p>
|
||||
<p>{tags["addr:postcode"]} {tags["addr:city"]}</p>
|
||||
|
||||
{#if tags.opening_hours}
|
||||
<OpeningHours hours={tags.opening_hours} {lat} lon={lng} />
|
||||
{/if}
|
||||
|
||||
{#if tags.amenity == "fuel"}
|
||||
<RequiresCapability capability="fuel">
|
||||
<FuelStation {tags} {lat} {lng} />
|
||||
</RequiresCapability>
|
||||
{/if}
|
||||
|
||||
<!-- any payment:* tag -->
|
||||
{#if Object.keys(tags).some(key => key.startsWith("payment:"))}
|
||||
<h3 class="text-lg font-bold mt-2">Payment Methods</h3>
|
||||
<ul style="display: flex; flex-wrap: wrap; gap: 0.5rem;">
|
||||
{#each Object.entries(tags).filter(([key]) => key.startsWith("payment:")) as [key, value]}
|
||||
<!-- <li>{key.replace("payment:", "")}: {value}</li> -->
|
||||
<Badge>{key.replace("payment:", "")}</Badge>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
<RequiresCapability capability="reviews">
|
||||
<Reviews lat={ellat} lng={ellng} />
|
||||
</RequiresCapability>
|
||||
|
||||
<span style="color: #acacac;">© OpenStreetMap</span>
|
||||
|
||||
<pre>{JSON.stringify(elements, null, 2)}</pre>
|
||||
{/if}
|
||||
{:catch err}
|
||||
<SidebarHeader onback={() => {
|
||||
pin.liftPin();
|
||||
}}>
|
||||
Dropped Pin
|
||||
</SidebarHeader>
|
||||
<p>Error: {err.message}</p>
|
||||
{/await}
|
||||
|
||||
<style>
|
||||
#actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
</style>
|
||||
2
src/lib/components/lnv/sidebar/InvalidSidebar.svelte
Normal file
2
src/lib/components/lnv/sidebar/InvalidSidebar.svelte
Normal file
@@ -0,0 +1,2 @@
|
||||
<h1>Error</h1>
|
||||
<p>Invalid sidebar configuration.</p>
|
||||
117
src/lib/components/lnv/sidebar/MainSidebar copy.svelte
Normal file
117
src/lib/components/lnv/sidebar/MainSidebar copy.svelte
Normal file
@@ -0,0 +1,117 @@
|
||||
<script lang="ts">
|
||||
import { BriefcaseIcon, HomeIcon } from "@lucide/svelte";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Input } from "../../ui/input";
|
||||
import { fly } from "svelte/transition";
|
||||
import { circInOut } from "svelte/easing";
|
||||
import { search, type Feature } from "$lib/services/Search";
|
||||
import { view } from "../sidebar.svelte";
|
||||
import { map, pin } from "../map.svelte";
|
||||
|
||||
function debounce<T>(getter: () => T, delay: number): () => T | undefined {
|
||||
let value = $state<T>();
|
||||
let timer: NodeJS.Timeout;
|
||||
$effect(() => {
|
||||
const newValue = getter(); // read here to subscribe to it
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => value = newValue, delay);
|
||||
return () => clearTimeout(timer);
|
||||
});
|
||||
return () => value;
|
||||
}
|
||||
|
||||
let typedText = $state("");
|
||||
let loading = $state(false);
|
||||
|
||||
let searchText = $derived.by(debounce(() => typedText, 300));
|
||||
let searchResults: Feature[] = $state([]);
|
||||
|
||||
$effect(() => {
|
||||
if(!searchText) {
|
||||
searchResults = [];
|
||||
return;
|
||||
}
|
||||
if (searchText.length > 0) {
|
||||
loading = true;
|
||||
search(searchText, 0, 0).then(results => {
|
||||
searchResults = results;
|
||||
loading = false;
|
||||
});
|
||||
} else {
|
||||
searchResults = [];
|
||||
}
|
||||
});
|
||||
|
||||
$inspect("searchText", searchText);
|
||||
</script>
|
||||
|
||||
<div id="search-progress" style="min-height: calc(3px + 3px); width: 100%; min-height: 3ch;">
|
||||
{#if loading}
|
||||
LOADING
|
||||
{/if}
|
||||
</div>
|
||||
<Input placeholder="Search..." bind:value={typedText} class="mb-2" />
|
||||
{#if searchResults.length == 0}
|
||||
<div id="saved" in:fly={{ y: 20, duration: 200, easing: circInOut }}>
|
||||
<Button variant="secondary" class="flex-1" onclick={() => {
|
||||
const home = localStorage.getItem("saved.home");
|
||||
if(!home) {
|
||||
alert("No home location saved.");
|
||||
return;
|
||||
}
|
||||
const {lat, lon} = JSON.parse(home);
|
||||
pin.dropPin(lat, lon);
|
||||
pin.showInfo();
|
||||
map.value?.flyTo({
|
||||
center: [lon, lat],
|
||||
zoom: 19
|
||||
});
|
||||
}}>
|
||||
<HomeIcon />
|
||||
Home
|
||||
</Button>
|
||||
<Button variant="secondary" class="flex-1" onclick={() => {
|
||||
const work = localStorage.getItem("saved.work");
|
||||
if(!work) {
|
||||
alert("No home location saved.");
|
||||
return;
|
||||
}
|
||||
const {lat, lon} = JSON.parse(work);
|
||||
pin.dropPin(lat, lon);
|
||||
pin.showInfo();
|
||||
map.value?.flyTo({
|
||||
center: [lon, lat],
|
||||
zoom: 19
|
||||
});
|
||||
}}>
|
||||
<BriefcaseIcon />
|
||||
Work
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div id="results" in:fly={{ y: 20, duration: 200, easing: circInOut }}>
|
||||
{#each searchResults as result}
|
||||
<Button variant="secondary" class="flex-1" onclick={() => {
|
||||
// view.switch("info", { feature: result });
|
||||
pin.dropPin(result.geometry.coordinates[1], result.geometry.coordinates[0]);
|
||||
pin.showInfo();
|
||||
map.value?.flyTo({
|
||||
center: [result.geometry.coordinates[0], result.geometry.coordinates[1]],
|
||||
zoom: 19
|
||||
});
|
||||
}}>
|
||||
{result.properties.name}
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
#saved {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
/* justify-content: space-evenly; */
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
64
src/lib/components/lnv/sidebar/MainSidebar.svelte
Normal file
64
src/lib/components/lnv/sidebar/MainSidebar.svelte
Normal file
@@ -0,0 +1,64 @@
|
||||
<script lang="ts">
|
||||
import { BriefcaseIcon, HomeIcon } from "@lucide/svelte";
|
||||
import { Button } from "../../ui/button";
|
||||
import { fly } from "svelte/transition";
|
||||
import { circInOut } from "svelte/easing";
|
||||
import { map, pin } from "../map.svelte";
|
||||
import VehicleSelector from "../VehicleSelector.svelte";
|
||||
import Post from "../Post.svelte";
|
||||
</script>
|
||||
|
||||
<div id="saved" class="mt-2 mb-2" in:fly={{ y: 20, duration: 200, easing: circInOut }}>
|
||||
<Button variant="secondary" class="flex-1" onclick={() => {
|
||||
const home = localStorage.getItem("saved.home");
|
||||
if(!home) {
|
||||
alert("No home location saved.");
|
||||
return;
|
||||
}
|
||||
const {lat, lon} = JSON.parse(home);
|
||||
pin.dropPin(lat, lon);
|
||||
pin.showInfo();
|
||||
map.value?.flyTo({
|
||||
center: [lon, lat],
|
||||
zoom: 19
|
||||
});
|
||||
}}>
|
||||
<HomeIcon />
|
||||
Home
|
||||
</Button>
|
||||
<Button variant="secondary" class="flex-1" onclick={() => {
|
||||
const work = localStorage.getItem("saved.work");
|
||||
if(!work) {
|
||||
alert("No home location saved.");
|
||||
return;
|
||||
}
|
||||
const {lat, lon} = JSON.parse(work);
|
||||
pin.dropPin(lat, lon);
|
||||
pin.showInfo();
|
||||
map.value?.flyTo({
|
||||
center: [lon, lat],
|
||||
zoom: 19
|
||||
});
|
||||
}}>
|
||||
<BriefcaseIcon />
|
||||
Work
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<VehicleSelector />
|
||||
|
||||
<div>
|
||||
<h2>In your area</h2>
|
||||
|
||||
<Post />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#saved {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
/* justify-content: space-evenly; */
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
92
src/lib/components/lnv/sidebar/RouteSidebar.svelte
Normal file
92
src/lib/components/lnv/sidebar/RouteSidebar.svelte
Normal file
@@ -0,0 +1,92 @@
|
||||
<script lang="ts">
|
||||
import { CircleArrowDown, CircleDotIcon, StarIcon } from "@lucide/svelte";
|
||||
import LocationSelect from "../LocationSelect.svelte";
|
||||
import Input from "$lib/components/ui/input/input.svelte";
|
||||
import SidebarHeader from "./SidebarHeader.svelte";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { createValhallaRequest } from "$lib/vehicles/ValhallaVehicles";
|
||||
import { drawAllRoutes, fetchRoute, removeAllRoutes, zoomToPoints } from "$lib/services/navigation/routing.svelte";
|
||||
import { ROUTING_SERVER } from "$lib/services/hosts";
|
||||
import { map } from "../map.svelte";
|
||||
import { view } from "../sidebar.svelte";
|
||||
import { DefaultVehicle, selectedVehicle } from "$lib/vehicles/vehicles.svelte";
|
||||
|
||||
let { from, to }: {
|
||||
from?: string,
|
||||
to?: string
|
||||
} = $props();
|
||||
|
||||
let fromLocation = $state(from || "");
|
||||
let toLocation = $state(to || "");
|
||||
|
||||
let routes: Trip[] | null = $state(null);
|
||||
|
||||
function formatTime(seconds: number) {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
// const secs = seconds % 60;
|
||||
return `${hours != 0 ? hours + "h " : ""}${minutes}min`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<SidebarHeader onback={() => {
|
||||
removeAllRoutes();
|
||||
}}>
|
||||
Route
|
||||
</SidebarHeader>
|
||||
|
||||
<span>Driving with <strong>{(selectedVehicle() ?? DefaultVehicle).name}</strong></span>
|
||||
<div class="flex flex-col gap-2 w-full mb-2">
|
||||
<div class="flex gap-2 items-center w-full">
|
||||
<CircleDotIcon />
|
||||
<Input bind:value={fromLocation} />
|
||||
</div>
|
||||
<div class="flex items-center justify-center w-full">
|
||||
<CircleArrowDown />
|
||||
</div>
|
||||
<div class="flex gap-2 items-center w-full">
|
||||
<CircleDotIcon />
|
||||
<Input bind:value={toLocation} />
|
||||
</div>
|
||||
</div>
|
||||
<Button onclick={async () => {
|
||||
const FROM: WorldLocation = fromLocation == "home" ? JSON.parse(localStorage.getItem("saved.home")!)
|
||||
: fromLocation == "work" ? JSON.parse(localStorage.getItem("saved.work")!)
|
||||
: {
|
||||
lat: parseFloat(fromLocation.split(",")[0]),
|
||||
lon: parseFloat(fromLocation.split(",")[1])
|
||||
};
|
||||
const TO: WorldLocation = toLocation == "home" ? JSON.parse(localStorage.getItem("saved.home")!)
|
||||
: toLocation == "work" ? JSON.parse(localStorage.getItem("saved.work")!)
|
||||
: {
|
||||
lat: parseFloat(toLocation.split(",")[0]),
|
||||
lon: parseFloat(toLocation.split(",")[1])
|
||||
};
|
||||
const req = createValhallaRequest(selectedVehicle() ?? DefaultVehicle, [FROM, TO]);
|
||||
const res = await fetchRoute(ROUTING_SERVER, req);
|
||||
routes = [
|
||||
res.trip,
|
||||
];
|
||||
for(const alternate of res.alternates) {
|
||||
if(alternate.trip) {
|
||||
routes.push(alternate.trip);
|
||||
}
|
||||
}
|
||||
drawAllRoutes(routes);
|
||||
zoomToPoints(FROM, TO, map.value!);
|
||||
}}>Calculate</Button>
|
||||
|
||||
{#if routes}
|
||||
<div class="mt-2 flex gap-2 flex-col">
|
||||
{#each routes as route, i (route?.summary?.length)}
|
||||
<Button variant="secondary" onclick={() => {
|
||||
view.switch("trip", { route });
|
||||
}}>
|
||||
{#if i == 0}
|
||||
<StarIcon />
|
||||
{/if}
|
||||
{route.summary.length}km - {formatTime(Math.round(route.summary.time))}
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
35
src/lib/components/lnv/sidebar/SearchSidebar.svelte
Normal file
35
src/lib/components/lnv/sidebar/SearchSidebar.svelte
Normal file
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import Button from "$lib/components/ui/button/button.svelte";
|
||||
import { circInOut } from "svelte/easing";
|
||||
import { fly } from "svelte/transition";
|
||||
import { map, pin } from "../map.svelte";
|
||||
import type { Feature } from "$lib/services/Search";
|
||||
import SidebarHeader from "./SidebarHeader.svelte";
|
||||
import { searchbar } from "../sidebar.svelte";
|
||||
|
||||
let { results, query }: {
|
||||
results: Feature[],
|
||||
query: string
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<SidebarHeader onback={() => {
|
||||
searchbar.text = "";
|
||||
}}>
|
||||
Search Results
|
||||
</SidebarHeader>
|
||||
<div id="results" class="mt-2" in:fly={{ y: 20, duration: 200, easing: circInOut }}>
|
||||
{#each results as result}
|
||||
<Button variant="secondary" class="flex-1" onclick={() => {
|
||||
// view.switch("info", { feature: result });
|
||||
pin.dropPin(result.geometry.coordinates[1], result.geometry.coordinates[0]);
|
||||
pin.showInfo();
|
||||
map.value?.flyTo({
|
||||
center: [result.geometry.coordinates[0], result.geometry.coordinates[1]],
|
||||
zoom: 19
|
||||
});
|
||||
}}>
|
||||
{result.properties.name}
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
20
src/lib/components/lnv/sidebar/SidebarHeader.svelte
Normal file
20
src/lib/components/lnv/sidebar/SidebarHeader.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import Button from "$lib/components/ui/button/button.svelte";
|
||||
import type { Snippet } from "svelte";
|
||||
import { view } from "../sidebar.svelte";
|
||||
|
||||
let { children, onback }: {
|
||||
children: Snippet,
|
||||
onback?: () => void,
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex gap-2 items-center mb-2">
|
||||
<Button variant="outline" onclick={() => {
|
||||
view.back();
|
||||
if (onback) {
|
||||
onback();
|
||||
}
|
||||
}}><</Button>
|
||||
<h2 class="text-lg font-bold flex gap-2 items-center">{@render children?.()}</h2>
|
||||
</div>
|
||||
51
src/lib/components/lnv/sidebar/TripSidebar.svelte
Normal file
51
src/lib/components/lnv/sidebar/TripSidebar.svelte
Normal file
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import SidebarHeader from "./SidebarHeader.svelte";
|
||||
import { drawRoute, removeAllRoutes, startRoute } from "$lib/services/navigation/routing.svelte";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { RouteIcon, SaveIcon, SendIcon } from "@lucide/svelte";
|
||||
import { map } from "../map.svelte";
|
||||
|
||||
let { route }: {
|
||||
route: Trip
|
||||
} = $props();
|
||||
|
||||
onMount(() => {
|
||||
removeAllRoutes();
|
||||
drawRoute(route);
|
||||
})
|
||||
</script>
|
||||
|
||||
<SidebarHeader onback={() => {
|
||||
removeAllRoutes();
|
||||
}}>
|
||||
Trip Details
|
||||
</SidebarHeader>
|
||||
|
||||
<div id="actions" class="flex gap-2">
|
||||
<Button onclick={async () => {
|
||||
await startRoute(route);
|
||||
requestAnimationFrame(() => {
|
||||
map.updateMapPadding();
|
||||
})
|
||||
}}>
|
||||
<RouteIcon />
|
||||
Start Navigation
|
||||
</Button>
|
||||
<Button variant="secondary" disabled>
|
||||
<SaveIcon />
|
||||
Save
|
||||
</Button>
|
||||
<Button variant="secondary" disabled>
|
||||
<SendIcon />
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2 mt-2">
|
||||
{#each route.legs[0].maneuvers as maneuver}
|
||||
<li>
|
||||
{maneuver.instruction}
|
||||
</li>
|
||||
{/each}
|
||||
</div>
|
||||
17
src/lib/components/ui/avatar/avatar-fallback.svelte
Normal file
17
src/lib/components/ui/avatar/avatar-fallback.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Avatar as AvatarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AvatarPrimitive.FallbackProps = $props();
|
||||
</script>
|
||||
|
||||
<AvatarPrimitive.Fallback
|
||||
bind:ref
|
||||
data-slot="avatar-fallback"
|
||||
class={cn("bg-muted flex size-full items-center justify-center rounded-full", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
17
src/lib/components/ui/avatar/avatar-image.svelte
Normal file
17
src/lib/components/ui/avatar/avatar-image.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Avatar as AvatarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AvatarPrimitive.ImageProps = $props();
|
||||
</script>
|
||||
|
||||
<AvatarPrimitive.Image
|
||||
bind:ref
|
||||
data-slot="avatar-image"
|
||||
class={cn("aspect-square size-full", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
17
src/lib/components/ui/avatar/avatar.svelte
Normal file
17
src/lib/components/ui/avatar/avatar.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Avatar as AvatarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AvatarPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<AvatarPrimitive.Root
|
||||
bind:ref
|
||||
data-slot="avatar"
|
||||
class={cn("relative flex size-8 shrink-0 overflow-hidden rounded-full", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
13
src/lib/components/ui/avatar/index.ts
Normal file
13
src/lib/components/ui/avatar/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import Root from "./avatar.svelte";
|
||||
import Image from "./avatar-image.svelte";
|
||||
import Fallback from "./avatar-fallback.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Image,
|
||||
Fallback,
|
||||
//
|
||||
Root as Avatar,
|
||||
Image as AvatarImage,
|
||||
Fallback as AvatarFallback,
|
||||
};
|
||||
50
src/lib/components/ui/badge/badge.svelte
Normal file
50
src/lib/components/ui/badge/badge.svelte
Normal file
@@ -0,0 +1,50 @@
|
||||
<script lang="ts" module>
|
||||
import { type VariantProps, tv } from "tailwind-variants";
|
||||
|
||||
export const badgeVariants = 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 inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-md border px-2 py-0.5 text-xs font-medium transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3",
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent",
|
||||
destructive:
|
||||
"bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white",
|
||||
outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
});
|
||||
|
||||
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { HTMLAnchorAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
href,
|
||||
class: className,
|
||||
variant = "default",
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAnchorAttributes> & {
|
||||
variant?: BadgeVariant;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<svelte:element
|
||||
this={href ? "a" : "span"}
|
||||
bind:this={ref}
|
||||
data-slot="badge"
|
||||
{href}
|
||||
class={cn(badgeVariants({ variant }), className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</svelte:element>
|
||||
2
src/lib/components/ui/badge/index.ts
Normal file
2
src/lib/components/ui/badge/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as Badge } from "./badge.svelte";
|
||||
export { badgeVariants, type BadgeVariant } from "./badge.svelte";
|
||||
80
src/lib/components/ui/button/button.svelte
Normal file
80
src/lib/components/ui/button/button.svelte
Normal file
@@ -0,0 +1,80 @@
|
||||
<script lang="ts" module>
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
|
||||
import { type VariantProps, tv } from "tailwind-variants";
|
||||
|
||||
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 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: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
destructive:
|
||||
"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",
|
||||
outline:
|
||||
"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",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
});
|
||||
|
||||
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
||||
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
|
||||
|
||||
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||
WithElementRef<HTMLAnchorAttributes> & {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
let {
|
||||
class: className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
ref = $bindable(null),
|
||||
href = undefined,
|
||||
type = "button",
|
||||
disabled,
|
||||
children,
|
||||
...restProps
|
||||
}: ButtonProps = $props();
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a
|
||||
bind:this={ref}
|
||||
data-slot="button"
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
href={disabled ? undefined : href}
|
||||
aria-disabled={disabled}
|
||||
role={disabled ? "link" : undefined}
|
||||
tabindex={disabled ? -1 : undefined}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
bind:this={ref}
|
||||
data-slot="button"
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
{type}
|
||||
{disabled}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
{/if}
|
||||
17
src/lib/components/ui/button/index.ts
Normal file
17
src/lib/components/ui/button/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import Root, {
|
||||
type ButtonProps,
|
||||
type ButtonSize,
|
||||
type ButtonVariant,
|
||||
buttonVariants,
|
||||
} from "./button.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
type ButtonProps as Props,
|
||||
//
|
||||
Root as Button,
|
||||
buttonVariants,
|
||||
type ButtonProps,
|
||||
type ButtonSize,
|
||||
type ButtonVariant,
|
||||
};
|
||||
20
src/lib/components/ui/card/card-action.svelte
Normal file
20
src/lib/components/ui/card/card-action.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-action"
|
||||
class={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
15
src/lib/components/ui/card/card-content.svelte
Normal file
15
src/lib/components/ui/card/card-content.svelte
Normal file
@@ -0,0 +1,15 @@
|
||||
<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="card-content" class={cn("px-6", className)} {...restProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
20
src/lib/components/ui/card/card-description.svelte
Normal file
20
src/lib/components/ui/card/card-description.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<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<HTMLParagraphElement>> = $props();
|
||||
</script>
|
||||
|
||||
<p
|
||||
bind:this={ref}
|
||||
data-slot="card-description"
|
||||
class={cn("text-muted-foreground text-sm", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</p>
|
||||
20
src/lib/components/ui/card/card-footer.svelte
Normal file
20
src/lib/components/ui/card/card-footer.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-footer"
|
||||
class={cn("[.border-t]:pt-6 flex items-center px-6", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
23
src/lib/components/ui/card/card-header.svelte
Normal file
23
src/lib/components/ui/card/card-header.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-header"
|
||||
class={cn(
|
||||
"@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
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
20
src/lib/components/ui/card/card-title.svelte
Normal file
20
src/lib/components/ui/card/card-title.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<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="card-title"
|
||||
class={cn("font-semibold leading-none", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
23
src/lib/components/ui/card/card.svelte
Normal file
23
src/lib/components/ui/card/card.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<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="card"
|
||||
class={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
25
src/lib/components/ui/card/index.ts
Normal file
25
src/lib/components/ui/card/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import Root from "./card.svelte";
|
||||
import Content from "./card-content.svelte";
|
||||
import Description from "./card-description.svelte";
|
||||
import Footer from "./card-footer.svelte";
|
||||
import Header from "./card-header.svelte";
|
||||
import Title from "./card-title.svelte";
|
||||
import Action from "./card-action.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Description,
|
||||
Footer,
|
||||
Header,
|
||||
Title,
|
||||
Action,
|
||||
//
|
||||
Root as Card,
|
||||
Content as CardContent,
|
||||
Description as CardDescription,
|
||||
Footer as CardFooter,
|
||||
Header as CardHeader,
|
||||
Title as CardTitle,
|
||||
Action as CardAction,
|
||||
};
|
||||
40
src/lib/components/ui/command/command-dialog.svelte
Normal file
40
src/lib/components/ui/command/command-dialog.svelte
Normal file
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import type { Command as CommandPrimitive, Dialog as DialogPrimitive } from "bits-ui";
|
||||
import type { Snippet } from "svelte";
|
||||
import Command from "./command.svelte";
|
||||
import * as Dialog from "$lib/components/ui/dialog/index.js";
|
||||
import type { WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
ref = $bindable(null),
|
||||
value = $bindable(""),
|
||||
title = "Command Palette",
|
||||
description = "Search for a command to run",
|
||||
portalProps,
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<DialogPrimitive.RootProps> &
|
||||
WithoutChildrenOrChild<CommandPrimitive.RootProps> & {
|
||||
portalProps?: DialogPrimitive.PortalProps;
|
||||
children: Snippet;
|
||||
title?: string;
|
||||
description?: string;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open {...restProps}>
|
||||
<Dialog.Header class="sr-only">
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
<Dialog.Description>{description}</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<Dialog.Content class="overflow-hidden p-0" {portalProps}>
|
||||
<Command
|
||||
class="**:data-[slot=command-input-wrapper]:h-12 [&_[data-command-group]:not([hidden])_~[data-command-group]]:pt-0 [&_[data-command-group]]:px-2 [&_[data-command-input-wrapper]_svg]:h-5 [&_[data-command-input-wrapper]_svg]:w-5 [&_[data-command-input]]:h-12 [&_[data-command-item]]:px-2 [&_[data-command-item]]:py-3 [&_[data-command-item]_svg]:h-5 [&_[data-command-item]_svg]:w-5"
|
||||
{...restProps}
|
||||
bind:value
|
||||
bind:ref
|
||||
{children}
|
||||
/>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
17
src/lib/components/ui/command/command-empty.svelte
Normal file
17
src/lib/components/ui/command/command-empty.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CommandPrimitive.EmptyProps = $props();
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.Empty
|
||||
bind:ref
|
||||
data-slot="command-empty"
|
||||
class={cn("py-6 text-center text-sm", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
32
src/lib/components/ui/command/command-group.svelte
Normal file
32
src/lib/components/ui/command/command-group.svelte
Normal file
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive, useId } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
heading,
|
||||
value,
|
||||
...restProps
|
||||
}: CommandPrimitive.GroupProps & {
|
||||
heading?: string;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.Group
|
||||
bind:ref
|
||||
data-slot="command-group"
|
||||
class={cn("text-foreground overflow-hidden p-1", className)}
|
||||
value={value ?? heading ?? `----${useId()}`}
|
||||
{...restProps}
|
||||
>
|
||||
{#if heading}
|
||||
<CommandPrimitive.GroupHeading
|
||||
class="text-muted-foreground px-2 py-1.5 text-xs font-medium"
|
||||
>
|
||||
{heading}
|
||||
</CommandPrimitive.GroupHeading>
|
||||
{/if}
|
||||
<CommandPrimitive.GroupItems {children} />
|
||||
</CommandPrimitive.Group>
|
||||
26
src/lib/components/ui/command/command-input.svelte
Normal file
26
src/lib/components/ui/command/command-input.svelte
Normal file
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from "bits-ui";
|
||||
import SearchIcon from "@lucide/svelte/icons/search";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
value = $bindable(""),
|
||||
...restProps
|
||||
}: CommandPrimitive.InputProps = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex h-9 items-center gap-2 border-b px-3" data-slot="command-input-wrapper">
|
||||
<SearchIcon class="size-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
data-slot="command-input"
|
||||
class={cn(
|
||||
"placeholder:text-muted-foreground outline-hidden flex h-10 w-full rounded-md bg-transparent py-3 text-sm disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
bind:ref
|
||||
{...restProps}
|
||||
bind:value
|
||||
/>
|
||||
</div>
|
||||
20
src/lib/components/ui/command/command-item.svelte
Normal file
20
src/lib/components/ui/command/command-item.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CommandPrimitive.ItemProps = $props();
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.Item
|
||||
bind:ref
|
||||
data-slot="command-item"
|
||||
class={cn(
|
||||
"aria-selected:bg-accent aria-selected:text-accent-foreground [&_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=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
20
src/lib/components/ui/command/command-link-item.svelte
Normal file
20
src/lib/components/ui/command/command-link-item.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CommandPrimitive.LinkItemProps = $props();
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.LinkItem
|
||||
bind:ref
|
||||
data-slot="command-item"
|
||||
class={cn(
|
||||
"aria-selected:bg-accent aria-selected:text-accent-foreground [&_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=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
17
src/lib/components/ui/command/command-list.svelte
Normal file
17
src/lib/components/ui/command/command-list.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CommandPrimitive.ListProps = $props();
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.List
|
||||
bind:ref
|
||||
data-slot="command-list"
|
||||
class={cn("max-h-[300px] scroll-py-1 overflow-y-auto overflow-x-hidden", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
17
src/lib/components/ui/command/command-separator.svelte
Normal file
17
src/lib/components/ui/command/command-separator.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CommandPrimitive.SeparatorProps = $props();
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.Separator
|
||||
bind:ref
|
||||
data-slot="command-separator"
|
||||
class={cn("bg-border -mx-1 h-px", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
20
src/lib/components/ui/command/command-shortcut.svelte
Normal file
20
src/lib/components/ui/command/command-shortcut.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
bind:this={ref}
|
||||
data-slot="command-shortcut"
|
||||
class={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</span>
|
||||
22
src/lib/components/ui/command/command.svelte
Normal file
22
src/lib/components/ui/command/command.svelte
Normal file
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(""),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CommandPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.Root
|
||||
bind:value
|
||||
bind:ref
|
||||
data-slot="command"
|
||||
class={cn(
|
||||
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
40
src/lib/components/ui/command/index.ts
Normal file
40
src/lib/components/ui/command/index.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Command as CommandPrimitive } from "bits-ui";
|
||||
|
||||
import Root from "./command.svelte";
|
||||
import Dialog from "./command-dialog.svelte";
|
||||
import Empty from "./command-empty.svelte";
|
||||
import Group from "./command-group.svelte";
|
||||
import Item from "./command-item.svelte";
|
||||
import Input from "./command-input.svelte";
|
||||
import List from "./command-list.svelte";
|
||||
import Separator from "./command-separator.svelte";
|
||||
import Shortcut from "./command-shortcut.svelte";
|
||||
import LinkItem from "./command-link-item.svelte";
|
||||
|
||||
const Loading = CommandPrimitive.Loading;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Dialog,
|
||||
Empty,
|
||||
Group,
|
||||
Item,
|
||||
LinkItem,
|
||||
Input,
|
||||
List,
|
||||
Separator,
|
||||
Shortcut,
|
||||
Loading,
|
||||
//
|
||||
Root as Command,
|
||||
Dialog as CommandDialog,
|
||||
Empty as CommandEmpty,
|
||||
Group as CommandGroup,
|
||||
Item as CommandItem,
|
||||
LinkItem as CommandLinkItem,
|
||||
Input as CommandInput,
|
||||
List as CommandList,
|
||||
Separator as CommandSeparator,
|
||||
Shortcut as CommandShortcut,
|
||||
Loading as CommandLoading,
|
||||
};
|
||||
7
src/lib/components/ui/dialog/dialog-close.svelte
Normal file
7
src/lib/components/ui/dialog/dialog-close.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: DialogPrimitive.CloseProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Close bind:ref data-slot="dialog-close" {...restProps} />
|
||||
39
src/lib/components/ui/dialog/dialog-content.svelte
Normal file
39
src/lib/components/ui/dialog/dialog-content.svelte
Normal file
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import XIcon from "@lucide/svelte/icons/x";
|
||||
import type { Snippet } from "svelte";
|
||||
import * as Dialog from "./index.js";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
portalProps,
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
|
||||
portalProps?: DialogPrimitive.PortalProps;
|
||||
children: Snippet;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<Dialog.Portal {...portalProps}>
|
||||
<Dialog.Overlay />
|
||||
<DialogPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="dialog-content"
|
||||
class={cn(
|
||||
"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
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<DialogPrimitive.Close
|
||||
class="ring-offset-background focus:ring-ring rounded-xs focus:outline-hidden absolute right-4 top-4 opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
|
||||
>
|
||||
<XIcon />
|
||||
<span class="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</Dialog.Portal>
|
||||
17
src/lib/components/ui/dialog/dialog-description.svelte
Normal file
17
src/lib/components/ui/dialog/dialog-description.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.DescriptionProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Description
|
||||
bind:ref
|
||||
data-slot="dialog-description"
|
||||
class={cn("text-muted-foreground text-sm", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
20
src/lib/components/ui/dialog/dialog-footer.svelte
Normal file
20
src/lib/components/ui/dialog/dialog-footer.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="dialog-footer"
|
||||
class={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
20
src/lib/components/ui/dialog/dialog-header.svelte
Normal file
20
src/lib/components/ui/dialog/dialog-header.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<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="dialog-header"
|
||||
class={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
20
src/lib/components/ui/dialog/dialog-overlay.svelte
Normal file
20
src/lib/components/ui/dialog/dialog-overlay.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.OverlayProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Overlay
|
||||
bind:ref
|
||||
data-slot="dialog-overlay"
|
||||
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}
|
||||
/>
|
||||
17
src/lib/components/ui/dialog/dialog-title.svelte
Normal file
17
src/lib/components/ui/dialog/dialog-title.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.TitleProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Title
|
||||
bind:ref
|
||||
data-slot="dialog-title"
|
||||
class={cn("text-lg font-semibold leading-none", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
7
src/lib/components/ui/dialog/dialog-trigger.svelte
Normal file
7
src/lib/components/ui/dialog/dialog-trigger.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: DialogPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Trigger bind:ref data-slot="dialog-trigger" {...restProps} />
|
||||
37
src/lib/components/ui/dialog/index.ts
Normal file
37
src/lib/components/ui/dialog/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
import Title from "./dialog-title.svelte";
|
||||
import Footer from "./dialog-footer.svelte";
|
||||
import Header from "./dialog-header.svelte";
|
||||
import Overlay from "./dialog-overlay.svelte";
|
||||
import Content from "./dialog-content.svelte";
|
||||
import Description from "./dialog-description.svelte";
|
||||
import Trigger from "./dialog-trigger.svelte";
|
||||
import Close from "./dialog-close.svelte";
|
||||
|
||||
const Root = DialogPrimitive.Root;
|
||||
const Portal = DialogPrimitive.Portal;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Title,
|
||||
Portal,
|
||||
Footer,
|
||||
Header,
|
||||
Trigger,
|
||||
Overlay,
|
||||
Content,
|
||||
Description,
|
||||
Close,
|
||||
//
|
||||
Root as Dialog,
|
||||
Title as DialogTitle,
|
||||
Portal as DialogPortal,
|
||||
Footer as DialogFooter,
|
||||
Header as DialogHeader,
|
||||
Trigger as DialogTrigger,
|
||||
Overlay as DialogOverlay,
|
||||
Content as DialogContent,
|
||||
Description as DialogDescription,
|
||||
Close as DialogClose,
|
||||
};
|
||||
7
src/lib/components/ui/drawer/drawer-close.svelte
Normal file
7
src/lib/components/ui/drawer/drawer-close.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: DrawerPrimitive.CloseProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Close bind:ref data-slot="drawer-close" {...restProps} />
|
||||
37
src/lib/components/ui/drawer/drawer-content.svelte
Normal file
37
src/lib/components/ui/drawer/drawer-content.svelte
Normal file
@@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
import DrawerOverlay from "./drawer-overlay.svelte";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
portalProps,
|
||||
children,
|
||||
...restProps
|
||||
}: DrawerPrimitive.ContentProps & {
|
||||
portalProps?: DrawerPrimitive.PortalProps;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Portal {...portalProps}>
|
||||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="drawer-content"
|
||||
class={cn(
|
||||
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
|
||||
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
|
||||
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
|
||||
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
|
||||
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
<div
|
||||
class="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block"
|
||||
></div>
|
||||
{@render children?.()}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPrimitive.Portal>
|
||||
17
src/lib/components/ui/drawer/drawer-description.svelte
Normal file
17
src/lib/components/ui/drawer/drawer-description.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DrawerPrimitive.DescriptionProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Description
|
||||
bind:ref
|
||||
data-slot="drawer-description"
|
||||
class={cn("text-muted-foreground text-sm", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
20
src/lib/components/ui/drawer/drawer-footer.svelte
Normal file
20
src/lib/components/ui/drawer/drawer-footer.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="drawer-footer"
|
||||
class={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
20
src/lib/components/ui/drawer/drawer-header.svelte
Normal file
20
src/lib/components/ui/drawer/drawer-header.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<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="drawer-header"
|
||||
class={cn("flex flex-col gap-1.5 p-4", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
12
src/lib/components/ui/drawer/drawer-nested.svelte
Normal file
12
src/lib/components/ui/drawer/drawer-nested.svelte
Normal file
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
|
||||
let {
|
||||
shouldScaleBackground = true,
|
||||
open = $bindable(false),
|
||||
activeSnapPoint = $bindable(null),
|
||||
...restProps
|
||||
}: DrawerPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.NestedRoot {shouldScaleBackground} bind:open bind:activeSnapPoint {...restProps} />
|
||||
20
src/lib/components/ui/drawer/drawer-overlay.svelte
Normal file
20
src/lib/components/ui/drawer/drawer-overlay.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DrawerPrimitive.OverlayProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Overlay
|
||||
bind:ref
|
||||
data-slot="drawer-overlay"
|
||||
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}
|
||||
/>
|
||||
17
src/lib/components/ui/drawer/drawer-title.svelte
Normal file
17
src/lib/components/ui/drawer/drawer-title.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DrawerPrimitive.TitleProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Title
|
||||
bind:ref
|
||||
data-slot="drawer-title"
|
||||
class={cn("text-foreground font-semibold", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
7
src/lib/components/ui/drawer/drawer-trigger.svelte
Normal file
7
src/lib/components/ui/drawer/drawer-trigger.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: DrawerPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Trigger bind:ref data-slot="drawer-trigger" {...restProps} />
|
||||
12
src/lib/components/ui/drawer/drawer.svelte
Normal file
12
src/lib/components/ui/drawer/drawer.svelte
Normal file
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
|
||||
let {
|
||||
shouldScaleBackground = true,
|
||||
open = $bindable(false),
|
||||
activeSnapPoint = $bindable(null),
|
||||
...restProps
|
||||
}: DrawerPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Root {shouldScaleBackground} bind:open bind:activeSnapPoint {...restProps} />
|
||||
41
src/lib/components/ui/drawer/index.ts
Normal file
41
src/lib/components/ui/drawer/index.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
|
||||
import Root from "./drawer.svelte";
|
||||
import Content from "./drawer-content.svelte";
|
||||
import Description from "./drawer-description.svelte";
|
||||
import Overlay from "./drawer-overlay.svelte";
|
||||
import Footer from "./drawer-footer.svelte";
|
||||
import Header from "./drawer-header.svelte";
|
||||
import Title from "./drawer-title.svelte";
|
||||
import NestedRoot from "./drawer-nested.svelte";
|
||||
import Close from "./drawer-close.svelte";
|
||||
import Trigger from "./drawer-trigger.svelte";
|
||||
|
||||
const Portal: typeof DrawerPrimitive.Portal = DrawerPrimitive.Portal;
|
||||
|
||||
export {
|
||||
Root,
|
||||
NestedRoot,
|
||||
Content,
|
||||
Description,
|
||||
Overlay,
|
||||
Footer,
|
||||
Header,
|
||||
Title,
|
||||
Trigger,
|
||||
Portal,
|
||||
Close,
|
||||
|
||||
//
|
||||
Root as Drawer,
|
||||
NestedRoot as DrawerNestedRoot,
|
||||
Content as DrawerContent,
|
||||
Description as DrawerDescription,
|
||||
Overlay as DrawerOverlay,
|
||||
Footer as DrawerFooter,
|
||||
Header as DrawerHeader,
|
||||
Title as DrawerTitle,
|
||||
Trigger as DrawerTrigger,
|
||||
Portal as DrawerPortal,
|
||||
Close as DrawerClose,
|
||||
};
|
||||
7
src/lib/components/ui/input/index.ts
Normal file
7
src/lib/components/ui/input/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from "./input.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Input,
|
||||
};
|
||||
51
src/lib/components/ui/input/input.svelte
Normal file
51
src/lib/components/ui/input/input.svelte
Normal file
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
type InputType = Exclude<HTMLInputTypeAttribute, "file">;
|
||||
|
||||
type Props = WithElementRef<
|
||||
Omit<HTMLInputAttributes, "type"> &
|
||||
({ type: "file"; files?: FileList } | { type?: InputType; files?: undefined })
|
||||
>;
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
type,
|
||||
files = $bindable(),
|
||||
class: className,
|
||||
...restProps
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if type === "file"}
|
||||
<input
|
||||
bind:this={ref}
|
||||
data-slot="input"
|
||||
class={cn(
|
||||
"selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-2 text-sm font-medium outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
type="file"
|
||||
bind:files
|
||||
bind:value
|
||||
{...restProps}
|
||||
/>
|
||||
{:else}
|
||||
<input
|
||||
bind:this={ref}
|
||||
data-slot="input"
|
||||
class={cn(
|
||||
"border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
{type}
|
||||
bind:value
|
||||
{...restProps}
|
||||
/>
|
||||
{/if}
|
||||
17
src/lib/components/ui/popover/index.ts
Normal file
17
src/lib/components/ui/popover/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||
import Content from "./popover-content.svelte";
|
||||
import Trigger from "./popover-trigger.svelte";
|
||||
const Root = PopoverPrimitive.Root;
|
||||
const Close = PopoverPrimitive.Close;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Trigger,
|
||||
Close,
|
||||
//
|
||||
Root as Popover,
|
||||
Content as PopoverContent,
|
||||
Trigger as PopoverTrigger,
|
||||
Close as PopoverClose,
|
||||
};
|
||||
29
src/lib/components/ui/popover/popover-content.svelte
Normal file
29
src/lib/components/ui/popover/popover-content.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
sideOffset = 4,
|
||||
align = "center",
|
||||
portalProps,
|
||||
...restProps
|
||||
}: PopoverPrimitive.ContentProps & {
|
||||
portalProps?: PopoverPrimitive.PortalProps;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<PopoverPrimitive.Portal {...portalProps}>
|
||||
<PopoverPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="popover-content"
|
||||
{sideOffset}
|
||||
{align}
|
||||
class={cn(
|
||||
"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 origin-(--bits-popover-content-transform-origin) outline-hidden z-50 w-72 rounded-md border p-4 shadow-md",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
17
src/lib/components/ui/popover/popover-trigger.svelte
Normal file
17
src/lib/components/ui/popover/popover-trigger.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: PopoverPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<PopoverPrimitive.Trigger
|
||||
bind:ref
|
||||
data-slot="popover-trigger"
|
||||
class={cn("", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
37
src/lib/components/ui/select/index.ts
Normal file
37
src/lib/components/ui/select/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
|
||||
import Group from "./select-group.svelte";
|
||||
import Label from "./select-label.svelte";
|
||||
import Item from "./select-item.svelte";
|
||||
import Content from "./select-content.svelte";
|
||||
import Trigger from "./select-trigger.svelte";
|
||||
import Separator from "./select-separator.svelte";
|
||||
import ScrollDownButton from "./select-scroll-down-button.svelte";
|
||||
import ScrollUpButton from "./select-scroll-up-button.svelte";
|
||||
import GroupHeading from "./select-group-heading.svelte";
|
||||
|
||||
const Root = SelectPrimitive.Root;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Group,
|
||||
Label,
|
||||
Item,
|
||||
Content,
|
||||
Trigger,
|
||||
Separator,
|
||||
ScrollDownButton,
|
||||
ScrollUpButton,
|
||||
GroupHeading,
|
||||
//
|
||||
Root as Select,
|
||||
Group as SelectGroup,
|
||||
Label as SelectLabel,
|
||||
Item as SelectItem,
|
||||
Content as SelectContent,
|
||||
Trigger as SelectTrigger,
|
||||
Separator as SelectSeparator,
|
||||
ScrollDownButton as SelectScrollDownButton,
|
||||
ScrollUpButton as SelectScrollUpButton,
|
||||
GroupHeading as SelectGroupHeading,
|
||||
};
|
||||
40
src/lib/components/ui/select/select-content.svelte
Normal file
40
src/lib/components/ui/select/select-content.svelte
Normal file
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import SelectScrollUpButton from "./select-scroll-up-button.svelte";
|
||||
import SelectScrollDownButton from "./select-scroll-down-button.svelte";
|
||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
sideOffset = 4,
|
||||
portalProps,
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.ContentProps> & {
|
||||
portalProps?: SelectPrimitive.PortalProps;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Portal {...portalProps}>
|
||||
<SelectPrimitive.Content
|
||||
bind:ref
|
||||
{sideOffset}
|
||||
data-slot="select-content"
|
||||
class={cn(
|
||||
"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-select-content-available-height) origin-(--bits-select-content-transform-origin) relative z-50 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border shadow-md data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
class={cn(
|
||||
"h-(--bits-select-anchor-height) min-w-(--bits-select-anchor-width) w-full scroll-my-1 p-1"
|
||||
)}
|
||||
>
|
||||
{@render children?.()}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
21
src/lib/components/ui/select/select-group-heading.svelte
Normal file
21
src/lib/components/ui/select/select-group-heading.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: ComponentProps<typeof SelectPrimitive.GroupHeading> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.GroupHeading
|
||||
bind:ref
|
||||
data-slot="select-group-heading"
|
||||
class={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</SelectPrimitive.GroupHeading>
|
||||
7
src/lib/components/ui/select/select-group.svelte
Normal file
7
src/lib/components/ui/select/select-group.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: SelectPrimitive.GroupProps = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Group data-slot="select-group" {...restProps} />
|
||||
38
src/lib/components/ui/select/select-item.svelte
Normal file
38
src/lib/components/ui/select/select-item.svelte
Normal file
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import CheckIcon from "@lucide/svelte/icons/check";
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
value,
|
||||
label,
|
||||
children: childrenProp,
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.ItemProps> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Item
|
||||
bind:ref
|
||||
{value}
|
||||
data-slot="select-item"
|
||||
class={cn(
|
||||
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 relative flex w-full cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-2 pr-8 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
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ selected, highlighted })}
|
||||
<span class="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
{#if selected}
|
||||
<CheckIcon class="size-4" />
|
||||
{/if}
|
||||
</span>
|
||||
{#if childrenProp}
|
||||
{@render childrenProp({ selected, highlighted })}
|
||||
{:else}
|
||||
{label || value}
|
||||
{/if}
|
||||
{/snippet}
|
||||
</SelectPrimitive.Item>
|
||||
20
src/lib/components/ui/select/select-label.svelte
Normal file
20
src/lib/components/ui/select/select-label.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {} = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="select-label"
|
||||
class={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SelectPrimitive.ScrollDownButtonProps> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
bind:ref
|
||||
data-slot="select-scroll-down-button"
|
||||
class={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||
{...restProps}
|
||||
>
|
||||
<ChevronDownIcon class="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
20
src/lib/components/ui/select/select-scroll-up-button.svelte
Normal file
20
src/lib/components/ui/select/select-scroll-up-button.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import ChevronUpIcon from "@lucide/svelte/icons/chevron-up";
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SelectPrimitive.ScrollUpButtonProps> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
bind:ref
|
||||
data-slot="select-scroll-up-button"
|
||||
class={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||
{...restProps}
|
||||
>
|
||||
<ChevronUpIcon class="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
18
src/lib/components/ui/select/select-separator.svelte
Normal file
18
src/lib/components/ui/select/select-separator.svelte
Normal file
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import type { Separator as SeparatorPrimitive } from "bits-ui";
|
||||
import { Separator } from "$lib/components/ui/separator/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: SeparatorPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<Separator
|
||||
bind:ref
|
||||
data-slot="select-separator"
|
||||
class={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
29
src/lib/components/ui/select/select-trigger.svelte
Normal file
29
src/lib/components/ui/select/select-trigger.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
size = "default",
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.TriggerProps> & {
|
||||
size?: "sm" | "default";
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Trigger
|
||||
bind:ref
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
class={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground 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:bg-input/30 dark:hover:bg-input/50 shadow-xs flex w-fit select-none items-center justify-between gap-2 whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<ChevronDownIcon class="size-4 opacity-50" />
|
||||
</SelectPrimitive.Trigger>
|
||||
7
src/lib/components/ui/separator/index.ts
Normal file
7
src/lib/components/ui/separator/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from "./separator.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Separator,
|
||||
};
|
||||
20
src/lib/components/ui/separator/separator.svelte
Normal file
20
src/lib/components/ui/separator/separator.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Separator as SeparatorPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: SeparatorPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<SeparatorPrimitive.Root
|
||||
bind:ref
|
||||
data-slot="separator"
|
||||
class={cn(
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
68
src/lib/services/MTSK.ts
Normal file
68
src/lib/services/MTSK.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { LNV_SERVER } from "./hosts";
|
||||
import { hasCapability } from "./lnv";
|
||||
|
||||
type Station = {
|
||||
id: string;
|
||||
name: string;
|
||||
brand: string;
|
||||
street: string;
|
||||
place: string;
|
||||
lat: number;
|
||||
lng: number;
|
||||
dist: number;
|
||||
diesel: number | false;
|
||||
e5: number | false;
|
||||
e10: number | false;
|
||||
isOpen: boolean;
|
||||
houseNumber: string;
|
||||
postCode: number;
|
||||
}
|
||||
|
||||
type StationsResponse = {
|
||||
ok: boolean;
|
||||
license: "CC BY 4.0 - https:\/\/creativecommons.tankerkoenig.de";
|
||||
data: "MTS-K";
|
||||
status: string;
|
||||
stations: Station[];
|
||||
};
|
||||
|
||||
type StationDetails = {
|
||||
openingTimes: StationOpeningTime[];
|
||||
overrides: string[];
|
||||
wholeDay: boolean;
|
||||
} & Station;
|
||||
|
||||
type StationOpeningTime = {
|
||||
text: string;
|
||||
start: string;
|
||||
end: string;
|
||||
};
|
||||
|
||||
type StationDetailsResponse = {
|
||||
ok: boolean;
|
||||
license: "CC BY 4.0 - https:\/\/creativecommons.tankerkoenig.de";
|
||||
data: "MTS-K";
|
||||
status: string;
|
||||
station: StationDetails;
|
||||
};
|
||||
|
||||
export async function getStations(lat: number, lon: number): Promise<StationsResponse> {
|
||||
if(!await hasCapability("fuel")) {
|
||||
throw new Error("Fuel capability is not available");
|
||||
}
|
||||
return await fetch(`${LNV_SERVER}/fuel/list?lat=${lat}&lng=${lon}&rad=1&sort=dist&type=all`).then(res => res.json());
|
||||
}
|
||||
|
||||
export async function getPrices(id: string) { // TODO: add type
|
||||
if(!await hasCapability("fuel")) {
|
||||
throw new Error("Fuel capability is not available");
|
||||
}
|
||||
return await fetch(`${LNV_SERVER}/fuel/prices?ids=${id}`).then(res => res.json());
|
||||
}
|
||||
|
||||
export async function getStationDetails(id: string): Promise<StationDetailsResponse> {
|
||||
if(!await hasCapability("fuel")) {
|
||||
throw new Error("Fuel capability is not available");
|
||||
}
|
||||
return await fetch(`${LNV_SERVER}/fuel/detail?id=${id}`).then(res => res.json());
|
||||
}
|
||||
65
src/lib/services/Overpass.ts
Normal file
65
src/lib/services/Overpass.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { OVERPASS_SERVER } from "./hosts";
|
||||
|
||||
export type OverpassResult = {
|
||||
elements: OverpassElement[];
|
||||
};
|
||||
|
||||
export type OverpassElement = {
|
||||
type: "node" | "way" | "relation";
|
||||
id: number;
|
||||
tags: Record<string, string>;
|
||||
lat?: number; // Only for nodes
|
||||
lon?: number; // Only for nodes
|
||||
nodes?: number[]; // Only for ways
|
||||
center?: {
|
||||
lat: number; // Only for relations
|
||||
lon: number; // Only for relations
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
[out:json];
|
||||
{{geocodeArea:<City>}}->.searchArea;
|
||||
// nwr(area.searchArea);
|
||||
(
|
||||
node(area.searchArea)["amenity"]["name"];
|
||||
way(area.searchArea)["amenity"]["name"];
|
||||
relation(area.searchArea)["amenity"]["name"];
|
||||
node(area.searchArea)["shop"]["name"];
|
||||
way(area.searchArea)["shop"]["name"];
|
||||
relation(area.searchArea)["shop"]["name"];
|
||||
node(area.searchArea)["building"]["building"!="garage"];
|
||||
way(area.searchArea)["building"]["building"!="garage"];
|
||||
node(area.searchArea)["amenity"="parking"];
|
||||
way(area.searchArea)["amenity"="parking"];
|
||||
);
|
||||
out geom;
|
||||
|
||||
[out:json];
|
||||
{{geocodeArea:<City>}};
|
||||
out geom;
|
||||
*/
|
||||
|
||||
export async function fetchPOI(
|
||||
lat: number,
|
||||
lon: number,
|
||||
radius: number,
|
||||
) {
|
||||
return await fetch(OVERPASS_SERVER, {
|
||||
method: "POST",
|
||||
body: `[out:json];
|
||||
(
|
||||
node(around:${radius}, ${lat}, ${lon})["amenity"]["name"];
|
||||
way(around:${radius}, ${lat}, ${lon})["amenity"]["name"];
|
||||
relation(around:${radius}, ${lat}, ${lon})["amenity"]["name"];
|
||||
node(around:${radius}, ${lat}, ${lon})["shop"]["name"];
|
||||
way(around:${radius}, ${lat}, ${lon})["shop"]["name"];
|
||||
relation(around:${radius}, ${lat}, ${lon})["shop"]["name"];
|
||||
node(around:${radius}, ${lat}, ${lon})["building"]["building"!="garage"];
|
||||
way(around:${radius}, ${lat}, ${lon})["building"]["building"!="garage"];
|
||||
node(around:${radius}, ${lat}, ${lon})["amenity"="parking"];
|
||||
way(around:${radius}, ${lat}, ${lon})["amenity"="parking"];
|
||||
);
|
||||
out center tags;`
|
||||
}).then(res => res.json() as Promise<OverpassResult>);
|
||||
}
|
||||
76
src/lib/services/Search.ts
Normal file
76
src/lib/services/Search.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
// import { Contacts } from "@capacitor-community/contacts";
|
||||
import { SEARCH_SERVER } from "./hosts";
|
||||
// import { Capacitor } from "@capacitor/core";
|
||||
|
||||
export type Feature = {
|
||||
type: "Feature",
|
||||
geometry: {
|
||||
coordinates: [number, number],
|
||||
type: "Point"
|
||||
},
|
||||
properties: {
|
||||
osm_key: string;
|
||||
osm_value: string;
|
||||
osm_id: number,
|
||||
city: string,
|
||||
country: string,
|
||||
name: string,
|
||||
street: string,
|
||||
housenumber: string,
|
||||
type: string,
|
||||
// There is more, but not needed atm
|
||||
}
|
||||
}
|
||||
|
||||
export async function searchPlaces(query: string, lat: number, lon: number): Promise<Feature[]> {
|
||||
const res = await fetch(SEARCH_SERVER + "/api/?q=" + query + "&lat=" + lat + "&lon=" + lon).then((res) => res.json());
|
||||
return res.features;
|
||||
}
|
||||
|
||||
export async function reverseGeocode(coord: WorldLocation): Promise<Feature[]> {
|
||||
const res = await fetch(SEARCH_SERVER + "/reverse?lat=" + coord.lat + "&lon=" + coord.lon).then((res) => res.json());
|
||||
return res.features;
|
||||
}
|
||||
|
||||
export async function search(query: string, lat: number, lon: number): Promise<Feature[]> {
|
||||
if(query.startsWith("@")) {
|
||||
// if(Capacitor.isNativePlatform()) {
|
||||
// return await searchContacts(query, lat, lon);
|
||||
// }
|
||||
return [];
|
||||
} else {
|
||||
return await searchPlaces(query, lat, lon);
|
||||
}
|
||||
}
|
||||
|
||||
// export async function searchContacts(query: string, lat: number, lon: number): Promise<Feature[]> {
|
||||
// console.log("Fetching contacts");
|
||||
// const allContacts = await Contacts.getContacts({
|
||||
// projection: {
|
||||
// name: true,
|
||||
// postalAddresses: true
|
||||
// }
|
||||
// });
|
||||
// console.log("Got contacts");
|
||||
// console.log(allContacts.contacts.map((contact) => contact.name?.display + " " + contact.postalAddresses?.[0]?.street));
|
||||
// const contacts = allContacts.contacts.filter((contact) => {
|
||||
// return contact.name?.display?.toLowerCase().includes(query.substring(1).toLowerCase());
|
||||
// });
|
||||
// console.log(contacts.map((contact) => contact.name?.display + " " + contact.postalAddresses?.[0]?.street));
|
||||
// const res = [];
|
||||
// for (const contact of contacts) {
|
||||
// const address = contact.postalAddresses?.[0];
|
||||
// if (!address) continue;
|
||||
// console.log("Fetching addr for " + contact.name?.display);
|
||||
// // Search for the address
|
||||
// const addressString = (address.street || "") + " " + (address.city || "") + " " + (address.country || "");
|
||||
// const addressRes = await searchPlaces(addressString, lat, lon);
|
||||
// console.log(addressRes);
|
||||
// if (addressRes.length > 0) {
|
||||
// const feature = addressRes[0];
|
||||
// feature.properties.name = contact.name?.display || "";
|
||||
// res.push(feature);
|
||||
// }
|
||||
// }
|
||||
// return res;
|
||||
// }
|
||||
7
src/lib/services/hosts.ts
Normal file
7
src/lib/services/hosts.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const MAP_SERVER = "https://tiles.openfreemap.org/styles/liberty";
|
||||
// export const MAP_SERVER = "https://tiles.map.picoscratch.de/styles/ofm/liberty.json";
|
||||
export const ROUTING_SERVER = "https://valhalla1.openstreetmap.de/";
|
||||
// export const ROUTING_SERVER = "https://routing.map.picoscratch.de";
|
||||
export const SEARCH_SERVER = "https://photon.komoot.io/";
|
||||
export const OVERPASS_SERVER = "https://overpass-api.de/api/interpreter";
|
||||
export const LNV_SERVER = "http://localhost:3000/api";
|
||||
100
src/lib/services/lnv.ts
Normal file
100
src/lib/services/lnv.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { createAuthClient } from "better-auth/client";
|
||||
import { usernameClient } from "better-auth/client/plugins";
|
||||
import { LNV_SERVER } from "./hosts";
|
||||
import type { User } from "better-auth";
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: LNV_SERVER + "/auth",
|
||||
plugins: [
|
||||
usernameClient()
|
||||
]
|
||||
})
|
||||
|
||||
export type Capabilities = ("auth" | "reviews" | "ai" | "fuel")[];
|
||||
export let capabilities: Capabilities = [];
|
||||
|
||||
export async function fetchConfig() {
|
||||
const res = await fetch(LNV_SERVER + "/config");
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to fetch capabilities: ${res.statusText}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
return data as { name: string; version: string; capabilities: Capabilities };
|
||||
}
|
||||
|
||||
export async function getCapabilities() {
|
||||
if (capabilities.length === 0) {
|
||||
const config = await fetchConfig();
|
||||
capabilities = config.capabilities;
|
||||
}
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
export async function hasCapability(capability: Capabilities[number]): Promise<boolean> {
|
||||
const caps = await getCapabilities();
|
||||
return caps.includes(capability);
|
||||
}
|
||||
|
||||
export type Review = {
|
||||
user_id: string;
|
||||
username: string;
|
||||
rating: number;
|
||||
comment: string;
|
||||
}
|
||||
export async function getReviews(location: WorldLocation) {
|
||||
if(!await hasCapability("reviews")) {
|
||||
throw new Error("Reviews capability is not available");
|
||||
}
|
||||
const res = await fetch(LNV_SERVER + `/reviews?lat=${location.lat}&lon=${location.lon}`);
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to fetch reviews: ${res.statusText}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
return data as Review[];
|
||||
}
|
||||
|
||||
export async function postReview(location: WorldLocation, review: Omit<Review, 'user_id' | 'username'>) {
|
||||
if(!await hasCapability("reviews")) {
|
||||
throw new Error("Reviews capability is not available");
|
||||
}
|
||||
const session = await authClient.getSession();
|
||||
if (session.error) {
|
||||
throw new Error("User is not authenticated");
|
||||
}
|
||||
const res = await fetch(LNV_SERVER + `/review`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${session.data?.session.token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...review,
|
||||
lat: location.lat,
|
||||
lon: location.lon
|
||||
})
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to post review: ${res.statusText}`);
|
||||
}
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
export async function ai(query: string, location?: WorldLocation) {
|
||||
if(!await hasCapability("ai")) {
|
||||
throw new Error("AI capability is not available");
|
||||
}
|
||||
const res = await fetch(LNV_SERVER + `/ai`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text: query,
|
||||
coords: location
|
||||
})
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to get AI response: ${res.statusText}`);
|
||||
}
|
||||
return await res.text();
|
||||
}
|
||||
70
src/lib/services/navigation/LaneDisplay.svelte
Normal file
70
src/lib/services/navigation/LaneDisplay.svelte
Normal file
@@ -0,0 +1,70 @@
|
||||
<script lang="ts">
|
||||
let { lane }: { lane: Lane } = $props();
|
||||
const knownDirections = [2, 4, 8, 16, 32, 64, 128, 256, 512, 1024];
|
||||
|
||||
async function fetchImage(bit: number) {
|
||||
if (knownDirections.includes(bit)) {
|
||||
return await fetch(`/img/lanes/${bit}.svg`).then(res => res.text());
|
||||
} else {
|
||||
return `<span>${bit}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
function loadImage(node: HTMLElement, bit: number) {
|
||||
fetchImage(bit).then(img => {
|
||||
node.innerHTML = img;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="lane">
|
||||
{#each Array(10).fill(0).map((_, i) => 1 << i) as bit}
|
||||
{#if lane.directions & bit}
|
||||
<div
|
||||
class="lane-image {lane.valid & bit ? 'valid' : ''} {lane.active & bit ? 'active' : ''}"
|
||||
use:loadImage={bit}
|
||||
></div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.lane {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
:global(.lane svg) {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
:global(.lane svg path) {
|
||||
stroke: #6b6b6b;
|
||||
stroke-width: 3;
|
||||
}
|
||||
|
||||
:global(.lane .active svg path) {
|
||||
stroke: #fff;
|
||||
}
|
||||
|
||||
:global(.lane .valid svg path) {
|
||||
stroke: #c0c0c0;
|
||||
}
|
||||
|
||||
:global(.lane-image > span) {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: #812020;
|
||||
}
|
||||
|
||||
:global(.lane-image.active > span) {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: #ff0000;
|
||||
}
|
||||
|
||||
:global(.lane-image.valid > span) {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: #cc2c2c;
|
||||
}
|
||||
</style>
|
||||
35
src/lib/services/navigation/LaneDisplay.ts
Normal file
35
src/lib/services/navigation/LaneDisplay.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
const knownDirections = [2, 4, 8, 16, 32, 64, 128, 256, 512, 1024];
|
||||
|
||||
export async function displayLane(lane: Lane) {
|
||||
const laneDiv = document.createElement("div");
|
||||
laneDiv.className = "lane";
|
||||
// Fetch all direction images from the bitmask
|
||||
for (let i = 0; i < 10; i++) {
|
||||
if (lane.directions & (1 << i)) {
|
||||
const bit = 1 << i;
|
||||
// Check if the bit is in the known directions
|
||||
let img = "";
|
||||
if (knownDirections.includes(bit)) {
|
||||
img = await fetch(`/img/lanes/${bit}.svg`).then(res => res.text());
|
||||
} else {
|
||||
img = `<span>${bit}</span>`;
|
||||
}
|
||||
const isValid = lane.valid & bit;
|
||||
const isActive = lane.active & bit;
|
||||
// Create a DOM element
|
||||
const laneImage = document.createElement("div");
|
||||
laneImage.className = "lane-image";
|
||||
laneImage.innerHTML = img;
|
||||
// Add the class to the laneDiv
|
||||
if (isValid) {
|
||||
laneImage.classList.add("valid");
|
||||
}
|
||||
if (isActive) {
|
||||
laneImage.classList.add("active");
|
||||
}
|
||||
// Add the lane image to the lane div
|
||||
laneDiv.appendChild(laneImage);
|
||||
}
|
||||
}
|
||||
return laneDiv;
|
||||
}
|
||||
22
src/lib/services/navigation/LanesDisplay.svelte
Normal file
22
src/lib/services/navigation/LanesDisplay.svelte
Normal file
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import LaneDisplay from "./LaneDisplay.svelte";
|
||||
|
||||
let { lanes }: { lanes: Lane[] | undefined } = $props();
|
||||
</script>
|
||||
|
||||
{#if lanes}
|
||||
<div id="lanes">
|
||||
{#each lanes as lane}
|
||||
<LaneDisplay {lane} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
#lanes {
|
||||
background-color: #1a1a1a;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
</style>
|
||||
46
src/lib/services/navigation/Maneuver.ts
Normal file
46
src/lib/services/navigation/Maneuver.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
export const maneuverTypes = [
|
||||
"none",
|
||||
"start",
|
||||
"startRight",
|
||||
"startLeft",
|
||||
"destination",
|
||||
"destinationRight",
|
||||
"destinationLeft",
|
||||
"becomes",
|
||||
"continue",
|
||||
"slightRight",
|
||||
"right",
|
||||
"sharpRight",
|
||||
"uTurnRight",
|
||||
"uTurnLeft",
|
||||
"sharpLeft",
|
||||
"left",
|
||||
"slightLeft",
|
||||
"rampStraight",
|
||||
"rampRight",
|
||||
"rampLeft",
|
||||
"exitRight",
|
||||
"exitLeft",
|
||||
"stayStraight",
|
||||
"stayRight",
|
||||
"stayLeft",
|
||||
"merge",
|
||||
"roundaboutEnter",
|
||||
"roundaboutExit",
|
||||
"ferryEnter",
|
||||
"ferryExit",
|
||||
"transit",
|
||||
"transitTransfer",
|
||||
"transitRemainsOn",
|
||||
"transitConnectionStart",
|
||||
"transitConnectionTransfer",
|
||||
"transitConnectionDestination",
|
||||
"postTransitConnectionDestination",
|
||||
"mergeRight",
|
||||
"mergeLeft",
|
||||
"elevatorEnter",
|
||||
"stepsEnter",
|
||||
"escalatorEnter",
|
||||
"buildingEnter",
|
||||
"buildingExit",
|
||||
];
|
||||
345
src/lib/services/navigation/Valhalla.old.ts
Normal file
345
src/lib/services/navigation/Valhalla.old.ts
Normal file
@@ -0,0 +1,345 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import maplibregl from "maplibre-gl";
|
||||
// import { maneuverTypes } from "./Maneuver";
|
||||
import { hideRouteStatus, updateRouteStatus } from "../../components/routestatus";
|
||||
import { NavigationLayer, removeAllNavigationLayers, updateNavigationLayer } from "./NavigationLayers";
|
||||
import { updateMapPadding } from "../../main";
|
||||
import say from "../TTS";
|
||||
import { ROUTING_SERVER } from "../servers";
|
||||
import { createValhallaRequest } from "./ValhallaRequest";
|
||||
import { Vehicle } from "../../components/vehicles";
|
||||
import { KeepAwake } from "@capacitor-community/keep-awake";
|
||||
import { getCurrentViewName, getSidebarView } from "../../components/sidebar/SidebarRegistry";
|
||||
// import { displayLane } from "./LaneDisplay";
|
||||
|
||||
export async function fetchRoute(vehicle: Vehicle, from: WorldLocation, to: WorldLocation): Promise<RouteResult> {
|
||||
// const req = {
|
||||
// locations: [
|
||||
// from,
|
||||
// to,
|
||||
// ],
|
||||
// costing: "motor_scooter",
|
||||
// units: "kilometers",
|
||||
// language: "de-DE",
|
||||
// alternates: 2,
|
||||
// costing_options: {
|
||||
// "motor_scooter": {
|
||||
// top_speed: 45,
|
||||
// },
|
||||
// },
|
||||
// };
|
||||
const req = createValhallaRequest(vehicle, [from, to]);
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
ROUTING_SERVER + "/route?json=" + JSON.stringify(req),
|
||||
).then((res) => res.json());
|
||||
|
||||
console.log(res);
|
||||
return res;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(e);
|
||||
throw new Error("Error fetching route");
|
||||
}
|
||||
}
|
||||
|
||||
let currentTrip: Trip | null = null;
|
||||
let instructionIdx = 0;
|
||||
let currentManeuver: Maneuver | null = null;
|
||||
let fromMarker: maplibregl.Marker;
|
||||
let toMarker: maplibregl.Marker;
|
||||
|
||||
export function getCurrentTrip() {
|
||||
return currentTrip;
|
||||
}
|
||||
|
||||
export function getCurrentManeuver() {
|
||||
return currentManeuver;
|
||||
}
|
||||
|
||||
function drawRoute(trip: Trip, name: NavigationLayer) {
|
||||
const geometry = decodePolyline(trip.legs[0].shape);
|
||||
console.log(geometry);
|
||||
updateNavigationLayer(name, geometry);
|
||||
}
|
||||
|
||||
export async function findRoute(vehicle: Vehicle, from: WorldLocation, to: WorldLocation): Promise<Trip[]> {
|
||||
fromMarker = new maplibregl.Marker()
|
||||
.setLngLat([from.lon, from.lat])
|
||||
.addTo(window.glmap);
|
||||
toMarker = new maplibregl.Marker()
|
||||
.setLngLat([to.lon, to.lat])
|
||||
.addTo(window.glmap);
|
||||
const route = await fetchRoute(vehicle, from, to);
|
||||
|
||||
let routes = [route.trip];
|
||||
if(route.alternates) {
|
||||
for(let i = 0; i < route.alternates.length; i++) {
|
||||
routes.push(route.alternates[i].trip);
|
||||
}
|
||||
}
|
||||
|
||||
drawAllRoutes(routes);
|
||||
|
||||
return routes;
|
||||
}
|
||||
|
||||
export async function drawAllRoutes(trips: Trip[]) {
|
||||
removeAllNavigationLayers();
|
||||
// if (result.alternates) {
|
||||
// for (let i = 0; i < result.alternates.length; i++) {
|
||||
// // @ts-ignore
|
||||
// drawRoute(result.alternates[i].trip, "al" + i);
|
||||
// }
|
||||
// }
|
||||
// drawRoute(result.trip, "route");
|
||||
for (let i = 1; i < trips.length; i++) {
|
||||
// @ts-ignore
|
||||
drawRoute(trips[i], "al" + (i - 1));
|
||||
}
|
||||
drawRoute(trips[0], "route");
|
||||
}
|
||||
|
||||
export async function drawSingleRoute(trip: Trip) {
|
||||
removeAllNavigationLayers();
|
||||
drawRoute(trip, "route");
|
||||
}
|
||||
|
||||
function getUserLocation(): WorldLocation {
|
||||
// @ts-ignore
|
||||
const lnglat = window.geolocate._userLocationDotMarker.getLngLat();
|
||||
return { lat: lnglat.lat, lon: lnglat.lng };
|
||||
}
|
||||
|
||||
// let putMarker = false;
|
||||
let pastRoute: WorldLocation[] = [];
|
||||
|
||||
// Check if the location is on the line between from and to (3 meter tolerance)
|
||||
function isOnLine(location: WorldLocation, from: WorldLocation, to: WorldLocation) {
|
||||
// Convert the 6-meter tolerance to degrees (approximation)
|
||||
const tolerance = 6 / 111320; // 1 degree latitude ≈ 111.32 km
|
||||
|
||||
// Calculate the vector components
|
||||
const dx = to.lon - from.lon;
|
||||
const dy = to.lat - from.lat;
|
||||
|
||||
// Calculate the projection of the location onto the line segment
|
||||
const t = ((location.lon - from.lon) * dx + (location.lat - from.lat) * dy) / (dx * dx + dy * dy);
|
||||
|
||||
// Clamp t to the range [0, 1] to ensure the projection is on the segment
|
||||
const clampedT = Math.max(0, Math.min(1, t));
|
||||
|
||||
// Calculate the closest point on the line segment
|
||||
const closestPoint = {
|
||||
lon: from.lon + clampedT * dx,
|
||||
lat: from.lat + clampedT * dy,
|
||||
};
|
||||
|
||||
// Calculate the distance from the location to the closest point
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(location.lon - closestPoint.lon, 2) + Math.pow(location.lat - closestPoint.lat, 2)
|
||||
);
|
||||
|
||||
// Check if the distance is within the tolerance
|
||||
return distance <= tolerance;
|
||||
}
|
||||
|
||||
function isOnShape(location: WorldLocation, shape: WorldLocation[]) {
|
||||
// Check if the location is on the line between from and to (3 meter tolerance)
|
||||
for (let i = 0; i < shape.length - 1; i++) {
|
||||
if (isOnLine(location, shape[i], shape[i + 1])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
let int: number | null = null;
|
||||
|
||||
export async function startNavigation(trip: Trip) {
|
||||
console.log("Start navigation");
|
||||
await KeepAwake.keepAwake();
|
||||
document.querySelector<HTMLDivElement>("#map-overlay")!.style.display = "";
|
||||
document.querySelector<HTMLBodyElement>("body")!.classList.add("isInTrip");
|
||||
updateMapPadding();
|
||||
instructionIdx = 0;
|
||||
pastRoute = [];
|
||||
// Delete all route and alternate route layers (if they exist)
|
||||
removeAllNavigationLayers();
|
||||
|
||||
// Add this route to the layer
|
||||
const geometry = decodePolyline(trip.legs[0].shape);
|
||||
updateNavigationLayer("route", geometry);
|
||||
|
||||
// @ts-ignore The types are not correct
|
||||
int = setInterval(() => {
|
||||
if(instructionIdx != 0) {
|
||||
// Only continue if the user location is at the end shape index of the current maneuver
|
||||
if(currentManeuver == null) {
|
||||
return;
|
||||
}
|
||||
const bgi = currentManeuver.begin_shape_index;
|
||||
const location = getUserLocation();
|
||||
const polyline = decodePolyline(trip.legs[0].shape);
|
||||
// const begin = polyline[bgi];
|
||||
|
||||
// const distance = Math.sqrt(
|
||||
// Math.pow(location.lat - begin.lat, 2) + Math.pow(location.lon - begin.lon, 2),
|
||||
// );
|
||||
|
||||
// console.log(distance);
|
||||
// // If within 3 meters of the end of the maneuver, go to the next maneuver
|
||||
// if (distance > 0.00003) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// Check if rerouting is needed/the user is off the route
|
||||
if (!isOnShape(location, polyline)) {
|
||||
console.log("Off route!");
|
||||
|
||||
updateRouteStatus({
|
||||
time: trip.summary.time,
|
||||
distance: trip.summary.length,
|
||||
currentManeuver: {
|
||||
...currentManeuver,
|
||||
instruction: "Off route!",
|
||||
},
|
||||
});
|
||||
// TODO: Implement rerouting
|
||||
} else {
|
||||
updateRouteStatus({
|
||||
time: trip.summary.time,
|
||||
distance: trip.summary.length,
|
||||
currentManeuver
|
||||
});
|
||||
}
|
||||
|
||||
// Check if the user finished the maneuver
|
||||
if (!isOnShape(location, polyline.slice(bgi))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update past route (for the past route line)
|
||||
// It needs to basically connect to the now removed part of the route
|
||||
// This is a bad example: pastRoute.push(...polyline.slice(0, bgi)); because it only adds a half of the route
|
||||
// This is a better example:
|
||||
// pastRoute.push(...polyline.slice(0, bgi + 1));
|
||||
pastRoute = polyline.slice(0, bgi + 1);
|
||||
|
||||
updateNavigationLayer("route-past", pastRoute.flat())
|
||||
|
||||
// Remove from shape begin to end from the route line
|
||||
const newShape = polyline.slice(bgi);
|
||||
updateNavigationLayer("route", newShape);
|
||||
}
|
||||
|
||||
instructionIdx++;
|
||||
// instructionIdx = 6;
|
||||
if (trip.legs[0].maneuvers.length <= instructionIdx) {
|
||||
stopNavigation();
|
||||
return;
|
||||
}
|
||||
const maneuver = trip.legs[0].maneuvers[instructionIdx];
|
||||
updateRouteStatus({
|
||||
time: trip.summary.time,
|
||||
distance: trip.summary.length,
|
||||
currentManeuver: trip.legs[0].maneuvers[instructionIdx],
|
||||
}, maneuver.lanes);
|
||||
currentManeuver = maneuver;
|
||||
|
||||
// document.querySelector<HTMLDivElement>("#lanes")!.innerHTML = "";
|
||||
// if(currentManeuver.lanes) {
|
||||
// console.log("Lanes: ", currentManeuver.lanes);
|
||||
// for(let i = 0; i < currentManeuver.lanes.length; i++) {
|
||||
// const lane = currentManeuver.lanes[i];
|
||||
// displayLane(lane).then((laneDiv) => {
|
||||
// // Add the lane div to the lanes container
|
||||
// document.querySelector<HTMLDivElement>("#lanes")!.appendChild(laneDiv);
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
// say(maneuver.instruction)
|
||||
// Say the verbal post instruction of the previous maneuver
|
||||
if (instructionIdx > 0) {
|
||||
const prevManeuver = trip.legs[0].maneuvers[instructionIdx - 1];
|
||||
if (prevManeuver.verbal_post_transition_instruction) {
|
||||
console.log("Saying: " + prevManeuver.verbal_post_transition_instruction);
|
||||
say(prevManeuver.verbal_post_transition_instruction);
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
updateRouteStatus({
|
||||
time: trip.summary.time,
|
||||
distance: trip.summary.length,
|
||||
currentManeuver: trip.legs[0].maneuvers[0],
|
||||
}, trip.legs[0].maneuvers[0].lanes);
|
||||
currentTrip = trip;
|
||||
}
|
||||
|
||||
export async function stopNavigation() {
|
||||
if(int) clearInterval(int);
|
||||
await KeepAwake.allowSleep();
|
||||
hideRouteStatus();
|
||||
document.querySelector<HTMLBodyElement>("body")!.classList.remove("isInTrip");
|
||||
updateMapPadding();
|
||||
currentTrip = null;
|
||||
currentManeuver = null;
|
||||
removeAllNavigationLayers();
|
||||
try {
|
||||
fromMarker.remove();
|
||||
toMarker.remove();
|
||||
} catch (e) {}
|
||||
getSidebarView(getCurrentViewName()!).switchTo();
|
||||
}
|
||||
|
||||
export function kmToString(km: number) {
|
||||
if (km < 1) {
|
||||
return Math.round(km * 100) + "m";
|
||||
}
|
||||
return Math.round(km) + "km";
|
||||
}
|
||||
|
||||
export function decodePolyline(encoded: string): WorldLocation[] {
|
||||
let points = [];
|
||||
let index = 0;
|
||||
let len = encoded.length;
|
||||
let lat = 0;
|
||||
let lng = 0;
|
||||
|
||||
while (index < len) {
|
||||
let shift = 0;
|
||||
let result = 0;
|
||||
let byte;
|
||||
do {
|
||||
byte = encoded.charCodeAt(index++) - 63;
|
||||
result |= (byte & 0x1f) << shift;
|
||||
shift += 5;
|
||||
} while (byte >= 0x20);
|
||||
|
||||
let deltaLat = (result & 1) ? ~(result >> 1) : (result >> 1);
|
||||
lat += deltaLat;
|
||||
|
||||
shift = 0;
|
||||
result = 0;
|
||||
do {
|
||||
byte = encoded.charCodeAt(index++) - 63;
|
||||
result |= (byte & 0x1f) << shift;
|
||||
shift += 5;
|
||||
} while (byte >= 0x20);
|
||||
|
||||
let deltaLng = (result & 1) ? ~(result >> 1) : (result >> 1);
|
||||
lng += deltaLng;
|
||||
|
||||
// Convert the latitude and longitude to decimal format with six digits of precision
|
||||
points.push({
|
||||
lat: lat / 1000000, // Divide by 1,000,000 for six digits of precision
|
||||
lon: lng / 1000000, // Divide by 1,000,000 for six digits of precision
|
||||
});
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
462
src/lib/services/navigation/ValhallaRequest.ts
Normal file
462
src/lib/services/navigation/ValhallaRequest.ts
Normal file
@@ -0,0 +1,462 @@
|
||||
export type ValhallaCosting =
|
||||
| "auto"
|
||||
| "bicycle"
|
||||
| "bus"
|
||||
| "bikeshare"
|
||||
| "truck"
|
||||
| "taxi"
|
||||
| "motor_scooter"
|
||||
| "multimodal"
|
||||
| "pedestrian";
|
||||
export type ValhallaRequest = {
|
||||
locations: WorldLocation[];
|
||||
costing: ValhallaCosting;
|
||||
units: "miles" | "kilometers";
|
||||
language: string;
|
||||
alternates: number;
|
||||
costing_options: ValhallaCostingOptions;
|
||||
turn_lanes: boolean;
|
||||
};
|
||||
export type GeneralCostingOptions = {
|
||||
/**
|
||||
* A penalty applied when transitioning between roads that do not have consistent
|
||||
* naming - in other words, no road names in common. This penalty can be used to
|
||||
* create simpler routes that tend to have fewer maneuvers or narrative guidance
|
||||
* instructions.
|
||||
* @default 5 seconds
|
||||
*/
|
||||
maneuver_penalty?: number;
|
||||
/**
|
||||
* A cost applied when a gate with undefined or private access is encountered.
|
||||
* This cost is added to the estimated time / elapsed time.
|
||||
* @default 30 seconds
|
||||
*/
|
||||
gate_cost?: number;
|
||||
/**
|
||||
* A penalty applied when a gate with no access information is on the road.
|
||||
* @default 300 seconds
|
||||
*/
|
||||
gate_penalty?: number;
|
||||
/**
|
||||
* A penalty applied when entering an road which is only allowed to enter if
|
||||
* necessary to reach the destination.
|
||||
*/
|
||||
destination_only_penalty?: number;
|
||||
/**
|
||||
* A cost applied when encountering an international border. This cost is added to the
|
||||
* estimated and elapsed times.
|
||||
* @default 600 seconds (10 minutes)
|
||||
*/
|
||||
country_crossing_cost?: number;
|
||||
/**
|
||||
* A penalty applied for a country crossing. This penalty can be used to create paths
|
||||
* that avoid spanning country boundaries.
|
||||
* @default 0
|
||||
*/
|
||||
country_crossing_penalty?: number;
|
||||
/**
|
||||
* A penalty applied for transition to generic service road.
|
||||
* @default 0 for trucks, 15 for cars, buses, motor scooters and motorcycles
|
||||
*/
|
||||
service_penalty?: number;
|
||||
};
|
||||
export type AutomobileCostingOptions = {
|
||||
/**
|
||||
* A penalty applied when a gate or bollard with access=private is encountered.
|
||||
* @default 450 seconds
|
||||
*/
|
||||
private_access_penalty?: number;
|
||||
/**
|
||||
* A cost applied when a toll booth is encountered. This cost is added to the
|
||||
* estimated and elapsed times.
|
||||
* @default 15 seconds
|
||||
*/
|
||||
toll_booth_cost?: number;
|
||||
/**
|
||||
* A penalty applied to the cost when a toll booth is encountered.
|
||||
* This penalty can be used to create paths that avoid toll roads.
|
||||
* @default 0
|
||||
*/
|
||||
toll_booth_penalty?: number;
|
||||
/**
|
||||
* A cost applied when entering a ferry. This cost is added to the estimated
|
||||
* and elapsed times.
|
||||
* @default 300 seconds (5 minutes)
|
||||
*/
|
||||
ferry_cost?: number;
|
||||
/**
|
||||
* This value indicates the willingness to take ferries. This is a range of values
|
||||
* between 0 and 1. Values near 0 attempt to avoid ferries and values near 1 will
|
||||
* favor ferries. Note that sometimes ferries are required to complete a route
|
||||
* so values of 0 are not guaranteed to avoid ferries entirely.
|
||||
* @default 0.5
|
||||
*/
|
||||
use_ferry?: number;
|
||||
/**
|
||||
* This value indicates the willingness to take highways. This is a range of values
|
||||
* between 0 and 1. Values near 0 attempt to avoid highways and values near 1 will
|
||||
* favor highways. Note that sometimes highways are required to complete a route
|
||||
* so values of 0 are not guaranteed to avoid highways entirely.
|
||||
* @default 1.0
|
||||
*/
|
||||
use_highways?: number;
|
||||
/**
|
||||
* This value indicates the willingness to take roads with tolls. This is a range of
|
||||
* values between 0 and 1. Values near 0 attempt to avoid tolls and values near 1 will
|
||||
* not attempt to avoid them. Note that sometimes roads with tolls are required to
|
||||
* complete a route so values of 0 are not guaranteed to avoid them entirely.
|
||||
* @default 0.5
|
||||
*/
|
||||
use_tolls?: number;
|
||||
/**
|
||||
* This value indicates the willingness to take living streets. This is a range of
|
||||
* values between 0 and 1. Values near 0 attempt to avoid living streets and values
|
||||
* near 1 will favor living streets. Note that sometimes living streets are required
|
||||
* to complete a route so values of 0 are not guaranteed to avoid living streets entirely.
|
||||
* @default 0 for trucks, 0.1 for cars, buses, motor scooters and motorcycles
|
||||
*/
|
||||
use_living_streets?: number;
|
||||
/**
|
||||
* This value indicates the willingness to take track roads. This is a range of values
|
||||
* between 0 and 1. Values near 0 attempt to avoid tracks and values near 1 will favor
|
||||
* tracks a little bit. Note that sometimes tracks are required to complete a route so values
|
||||
* of 0 are not guaranteed to avoid tracks entirely.
|
||||
* @default 0 for autos, 0.5 for motor scooters and motorcycles
|
||||
*/
|
||||
use_tracks?: number;
|
||||
/**
|
||||
* A factor that modifies (multiplies) the cost when generic service roads
|
||||
* are encountered.
|
||||
* @default 1
|
||||
*/
|
||||
service_factor?: number;
|
||||
/**
|
||||
* Changes the metric to quasi-shortest, i.e. purely distance-based costing.
|
||||
* Note, this will disable all other costings & penalties. Also note, shortest will not
|
||||
* disable hierarchy pruning, leading to potentially sub-optimal routes for some costing models.
|
||||
* @default false
|
||||
*/
|
||||
shortest?: boolean;
|
||||
/**
|
||||
* A factor that allows controlling the contribution of distance and time to the route costs.
|
||||
* The value is in range between 0 and 1, where 0 only takes time into account (default)
|
||||
* and 1 only distance. A factor of 0.5 will weight them roughly equally.
|
||||
* Note: this costing is currently only available for auto costing.
|
||||
*/
|
||||
use_distance?: boolean;
|
||||
/**
|
||||
* Disable hierarchies to calculate the actual optimal route.
|
||||
* Note: This could be quite a performance drainer so there is a upper limit of distance.
|
||||
* If the upper limit is exceeded, this option will always be false.
|
||||
* @default false
|
||||
*/
|
||||
disable_hierarchy_pruning?: boolean;
|
||||
/**
|
||||
* Top speed the vehicle can go. Also used to avoid roads with higher speeds than this value.
|
||||
* top_speed must be between 10 and 252 KPH.
|
||||
* @default 120 KPH for truck, 140 KPH for auto and bus, 45 KPH for motor_scooter
|
||||
*/
|
||||
top_speed?: number;
|
||||
/**
|
||||
* Fixed speed the vehicle can go. Used to override the calculated speed.
|
||||
* Can be useful if speed of vehicle is known. fixed_speed must be between 1 and 252 KPH.
|
||||
* The default value is 0 KPH which disables fixed speed and falls back to the standard
|
||||
* calculated speed based on the road attribution.
|
||||
* @default 0
|
||||
*/
|
||||
fixed_speed?: number;
|
||||
/**
|
||||
* If set to true, ignores all closures, marked due to live traffic closures, during routing.
|
||||
* Note: This option cannot be set if location.search_filter.exclude_closures is also specified
|
||||
* in the request and will return an error if it is.
|
||||
* @default false
|
||||
*/
|
||||
ignore_closures?: boolean;
|
||||
/**
|
||||
* A factor that penalizes the cost when traversing a closed edge
|
||||
* (eg: if search_filter.exclude_closures is false for origin and/or destination location
|
||||
* and the route starts/ends on closed edges). Its value can range from 1.0 - don't penalize closed edges,
|
||||
* to 10.0 - apply high cost penalty to closed edges.
|
||||
* Note: This factor is applicable only for motorized modes of transport, i.e auto, motorcycle, motor_scooter, bus, truck & taxi.
|
||||
* @default 9.0
|
||||
*/
|
||||
closure_factor?: number;
|
||||
/**
|
||||
* If set to true, ignores any restrictions (e.g. turn/dimensional/conditional restrictions).
|
||||
* Especially useful for matching GPS traces to the road network regardless of restrictions.
|
||||
* @default false
|
||||
*/
|
||||
ignore_restrictions?: boolean;
|
||||
/**
|
||||
* If set to true, ignores one-way restrictions. Especially useful for matching GPS traces
|
||||
* to the road network ignoring uni-directional traffic rules.
|
||||
* Not included in ignore_restrictions option.
|
||||
* @default false
|
||||
*/
|
||||
ignore_oneways?: boolean;
|
||||
/**
|
||||
* Similar to ignore_restrictions, but will respect restrictions that impact vehicle safety,
|
||||
* such as weight and size restrictions.
|
||||
* @default false
|
||||
*/
|
||||
ignore_non_vehicular_restrictions?: boolean;
|
||||
/**
|
||||
* Will ignore mode-specific access tags. Especially useful for matching GPS traces to the
|
||||
* road network regardless of restrictions.
|
||||
* @default false
|
||||
*/
|
||||
ignore_access?: boolean;
|
||||
/**
|
||||
* Will ignore construction tags. Only works when the include_construction option is set
|
||||
* before building the graph. Useful for planning future routes.
|
||||
* @default false
|
||||
*/
|
||||
ignore_construction?: boolean;
|
||||
/**
|
||||
* Will determine which speed sources are used, if available. A list of strings with the following possible values:
|
||||
* @default ["freeflow", "constrained", "predicted", "current"]
|
||||
*/
|
||||
speed_types?: ("freeflow" | "constrained" | "predicted" | "current")[];
|
||||
/**
|
||||
* Pass custom hierarchy limits along with this request (read more about the tile hierarchy here).
|
||||
* Needs to be an object with mandatory keys 1 and 2, each value is another object containing
|
||||
* numerical values for max_up_transitions and expand_within_distance. The service may either
|
||||
* clamp these values or disallow modifying hierarchy limits via the request parameters entirely.
|
||||
* @todo
|
||||
*/
|
||||
hierarchy_limits?: void;
|
||||
} & GeneralCostingOptions;
|
||||
export type OtherCostingOptions = {
|
||||
/**
|
||||
* The height of the vehicle (in meters).
|
||||
* @default 1.9 for car, bus, taxi and 4.11 for truck
|
||||
*/
|
||||
height?: number;
|
||||
/**
|
||||
* The width of the vehicle (in meters).
|
||||
* @default 1.6 for car, bus, taxi and 2.6 for truck
|
||||
*/
|
||||
width?: number;
|
||||
/**
|
||||
* This value indicates whether or not the path may include unpaved roads.
|
||||
* If exclude_unpaved is set to 1 it is allowed to start and end with unpaved roads,
|
||||
* but is not allowed to have them in the middle of the route path, otherwise they are allowed.
|
||||
* @default false
|
||||
*/
|
||||
exclude_unpaved?: boolean;
|
||||
/**
|
||||
* A boolean value which indicates the desire to avoid routes with cash-only tolls.
|
||||
* @default false
|
||||
*/
|
||||
exclude_cash_only_tolls?: boolean;
|
||||
/**
|
||||
* A boolean value which indicates the desire to include HOV roads with a 2-occupant requirement
|
||||
* in the route when advantageous.
|
||||
* @default false
|
||||
*/
|
||||
include_hov2?: boolean;
|
||||
/**
|
||||
* A boolean value which indicates the desire to include HOV roads with a 3-occupant requirement
|
||||
* in the route when advantageous.
|
||||
* @default false
|
||||
*/
|
||||
include_hov3?: boolean;
|
||||
/**
|
||||
* A boolean value which indicates the desire to include tolled HOV roads which require the driver
|
||||
* to pay a toll if the occupant requirement isn't met.
|
||||
* @default false
|
||||
*/
|
||||
include_hot?: boolean;
|
||||
};
|
||||
/**
|
||||
* The type of the bicycle.
|
||||
* Road: a road-style bicycle with narrow tires that is generally lightweight and designed for speed on paved surfaces.
|
||||
* Hybrid or City: a bicycle made mostly for city riding or casual riding on roads and paths with good surfaces.
|
||||
* Cross: a cyclo-cross bicycle, which is similar to a road bicycle but with wider tires suitable to rougher surfaces.
|
||||
* Mountain: a mountain bicycle suitable for most surfaces but generally heavier and slower on paved surfaces.
|
||||
*/
|
||||
export type BicycleType = "Road" | "Hybrid" | "City" | "Mountain";
|
||||
export type BicycleCostingOptions = {
|
||||
/**
|
||||
* @default "Hybrid"
|
||||
*/
|
||||
bicycle_type?: BicycleType;
|
||||
/**
|
||||
* Cycling speed is the average travel speed along smooth, flat roads.
|
||||
* This is meant to be the speed a rider can comfortably maintain over the desired distance of the route.
|
||||
* It can be modified (in the costing method) by surface type in conjunction with bicycle type and (coming soon)
|
||||
* by hilliness of the road section. When no speed is specifically provided, the default speed is determined
|
||||
* by the bicycle type and are as follows: Road = 25 KPH (15.5 MPH), Cross = 20 KPH (13 MPH),
|
||||
* Hybrid/City = 18 KPH (11.5 MPH), and Mountain = 16 KPH (10 MPH).
|
||||
*/
|
||||
cycling_speed?: number;
|
||||
/**
|
||||
* A cyclist's propensity to use roads alongside other vehicles.
|
||||
* This is a range of values from 0 to 1, where 0 attempts to avoid roads
|
||||
* and stay on cycleways and paths, and 1 indicates the rider is more
|
||||
* comfortable riding on roads. Based on the use_roads factor, roads with
|
||||
* certain classifications and higher speeds are penalized in an attempt to
|
||||
* avoid them when finding the best path.
|
||||
* @default 0.5
|
||||
*/
|
||||
use_roads?: number;
|
||||
/**
|
||||
* A cyclist's desire to tackle hills in their routes.
|
||||
* This is a range of values from 0 to 1, where 0 attempts to avoid
|
||||
* hills and steep grades even if it means a longer (time and distance) path,
|
||||
* while 1 indicates the rider does not fear hills and steeper grades.
|
||||
* Based on the use_hills factor, penalties are applied to roads based on
|
||||
* elevation change and grade. These penalties help the path avoid hilly
|
||||
* roads in favor of flatter roads or less steep grades where available.
|
||||
* Note that it is not always possible to find alternate paths to avoid
|
||||
* hills (for example when route locations are in mountainous areas).
|
||||
* @default 0.5
|
||||
*/
|
||||
use_hills?: number;
|
||||
/**
|
||||
* This value indicates the willingness to take ferries.
|
||||
* This is a range of values between 0 and 1.
|
||||
* Values near 0 attempt to avoid ferries and values near 1 will
|
||||
* favor ferries. Note that sometimes ferries are required to complete
|
||||
* a route so values of 0 are not guaranteed to avoid ferries entirely.
|
||||
* @default 0.5
|
||||
*/
|
||||
use_ferry?: number;
|
||||
/**
|
||||
* This value indicates the willingness to take living streets.
|
||||
* This is a range of values between 0 and 1. Values near 0 attempt
|
||||
* to avoid living streets and values from 0.5 to 1 will currently
|
||||
* have no effect on route selection. Note that sometimes living
|
||||
* streets are required to complete a route so values of 0 are not
|
||||
* guaranteed to avoid living streets entirely.
|
||||
* @default 0.5
|
||||
*/
|
||||
use_living_streets?: number;
|
||||
/**
|
||||
* This value is meant to represent how much a cyclist wants to avoid
|
||||
* roads with poor surfaces relative to the bicycle type being used.
|
||||
* This is a range of values between 0 and 1. When the value is 0,
|
||||
* there is no penalization of roads with different surface types;
|
||||
* only bicycle speed on each surface is taken into account. As
|
||||
* the value approaches 1, roads with poor surfaces for the bike are
|
||||
* penalized heavier so that they are only taken if they significantly
|
||||
* improve travel time. When the value is equal to 1, all bad surfaces
|
||||
* are completely disallowed from routing, including start and end points.
|
||||
* @default 0.25
|
||||
*/
|
||||
avoid_bad_surfaces?: number;
|
||||
/**
|
||||
* This value is useful when bikeshare is chosen as travel mode.
|
||||
* It is meant to give the time will be used to return a rental bike.
|
||||
* This value will be displayed in the final directions and used to
|
||||
* calculate the whole duration.
|
||||
* @default 120 seconds
|
||||
*/
|
||||
bss_return_cost?: number;
|
||||
/**
|
||||
* This value is useful when bikeshare is chosen as travel mode.
|
||||
* It is meant to describe the potential effort to return a rental bike.
|
||||
* This value won't be displayed and used only inside of the algorithm.
|
||||
*/
|
||||
bss_return_penalty?: number;
|
||||
/**
|
||||
* Changes the metric to quasi-shortest, i.e. purely distance-based costing.
|
||||
* Note, this will disable all other costings & penalties. Also note, shortest
|
||||
* will not disable hierarchy pruning, leading to potentially sub-optimal routes
|
||||
* for some costing models.
|
||||
* @default false
|
||||
*/
|
||||
shortest?: boolean;
|
||||
|
||||
};
|
||||
export type BikeshareCostingOptions = {}; // TODO
|
||||
export type MotorScooterCostingOptions = {
|
||||
/**
|
||||
* A rider's propensity to use primary roads.
|
||||
* This is a range of values from 0 to 1, where 0
|
||||
* attempts to avoid primary roads, and 1 indicates
|
||||
* the rider is more comfortable riding on primary roads.
|
||||
* Based on the use_primary factor, roads with certain
|
||||
* classifications and higher speeds are penalized in an
|
||||
* attempt to avoid them when finding the best path.
|
||||
* @default 0.5
|
||||
*/
|
||||
use_primary?: boolean;
|
||||
/**
|
||||
* A rider's desire to tackle hills in their routes.
|
||||
* This is a range of values from 0 to 1, where 0
|
||||
* attempts to avoid hills and steep grades even if
|
||||
* it means a longer (time and distance) path, while 1
|
||||
* indicates the rider does not fear hills and steeper grades.
|
||||
* Based on the use_hills factor, penalties are applied
|
||||
* to roads based on elevation change and grade. These
|
||||
* penalties help the path avoid hilly roads in favor of flatter
|
||||
* roads or less steep grades where available. Note that it is
|
||||
* not always possible to find alternate paths to avoid hills
|
||||
* (for example when route locations are in mountainous areas).
|
||||
* @default 0.5
|
||||
*/
|
||||
use_hills?: boolean;
|
||||
} & AutomobileCostingOptions;
|
||||
export type MultimodalCostingOptions = {}; // TODO
|
||||
export type PedestrianCostingOptions = {}; // TODO
|
||||
export type TruckCostingOptions = {
|
||||
/**
|
||||
* The length of the truck (in meters).
|
||||
* @default 21.64
|
||||
*/
|
||||
length?: number;
|
||||
/**
|
||||
* The weight of the truck (in metric tons).
|
||||
* @default 21.77
|
||||
*/
|
||||
weight?: number;
|
||||
/**
|
||||
* The axle load of the truck (in metric tons).
|
||||
* @default 9.07
|
||||
*/
|
||||
axle_load?: number;
|
||||
/**
|
||||
* The axle count of the truck.
|
||||
* @default 5
|
||||
*/
|
||||
axle_count?: number;
|
||||
/**
|
||||
* A value indicating if the truck is carrying hazardous materials.
|
||||
* @default false
|
||||
*/
|
||||
hazmat?: boolean;
|
||||
/**
|
||||
* A penalty applied to roads with no HGV/truck access.
|
||||
* If set to a value less than 43200 seconds, HGV will
|
||||
* be allowed on these roads and the penalty applies.
|
||||
* Default 43200, i.e. trucks are not allowed.
|
||||
* @default 43200
|
||||
*/
|
||||
hgv_no_access_penalty?: number;
|
||||
/**
|
||||
* A penalty (in seconds) which is applied when going to residential or service roads.
|
||||
* @default 30 seconds
|
||||
*/
|
||||
low_class_penalty?: number;
|
||||
/**
|
||||
* This value is a range of values from 0 to 1, where 0 indicates a light preference
|
||||
* for streets marked as truck routes, and 1 indicates that streets not marked as truck
|
||||
* routes should be avoided. This information is derived from the hgv=designated tag.
|
||||
* Note that even with values near 1, there is no guarantee the returned route will
|
||||
* include streets marked as truck routes.
|
||||
* @default 0
|
||||
*/
|
||||
use_truck_route?: boolean;
|
||||
};
|
||||
export type ValhallaCostingOptions = {
|
||||
auto?: AutomobileCostingOptions & OtherCostingOptions;
|
||||
bicycle?: BicycleCostingOptions;
|
||||
bus?: AutomobileCostingOptions & OtherCostingOptions;
|
||||
bikeshare?: BikeshareCostingOptions;
|
||||
truck?: AutomobileCostingOptions & OtherCostingOptions & TruckCostingOptions;
|
||||
taxi?: OtherCostingOptions;
|
||||
motor_scooter?: MotorScooterCostingOptions;
|
||||
multimodal?: MultimodalCostingOptions;
|
||||
pedestrian?: PedestrianCostingOptions;
|
||||
};
|
||||
81
src/lib/services/navigation/navigation.d.ts
vendored
Normal file
81
src/lib/services/navigation/navigation.d.ts
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
type Language = "de-DE" | "en-US";
|
||||
type WorldLocation = { lat: number; lon: number };
|
||||
type Units = "kilometers" | "miles";
|
||||
|
||||
type RouteResult = {
|
||||
alternates?: {
|
||||
trip: Trip;
|
||||
}[];
|
||||
trip: Trip;
|
||||
}
|
||||
|
||||
type Trip = {
|
||||
language: Language;
|
||||
legs: Leg[];
|
||||
status: number;
|
||||
status_message: string;
|
||||
summary: Summary;
|
||||
units: Units;
|
||||
locations: WorldLocation[];
|
||||
};
|
||||
|
||||
type Leg = {
|
||||
maneuvers: Maneuver[];
|
||||
shape: string;
|
||||
summary: Summary;
|
||||
locations: WorldLocation[];
|
||||
}
|
||||
|
||||
type Summary = {
|
||||
cost: number;
|
||||
has_ferry: boolean;
|
||||
has_highway: boolean;
|
||||
has_time_restrictions: boolean;
|
||||
has_toll: boolean;
|
||||
length: number;
|
||||
max_lat: number;
|
||||
max_lon: number;
|
||||
min_lat: number;
|
||||
min_lon: number;
|
||||
time: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Direction bitmask:
|
||||
* 0 = Empty
|
||||
* 1 = None
|
||||
* 2 = Through/Straight X
|
||||
* 4 = SharpLeft X
|
||||
* 8 = Left X
|
||||
* 16 = SlightLeft X
|
||||
* 32 = SlightRight X
|
||||
* 64 = Right X
|
||||
* 128 = SharpRight X
|
||||
* 256 = Reverse/U-turn X
|
||||
* 512 = MergeToLeft
|
||||
* 1024 = MergeToRight
|
||||
*/
|
||||
type Lane = {
|
||||
directions: number;
|
||||
valid: number;
|
||||
active: number;
|
||||
};
|
||||
|
||||
type Maneuver = {
|
||||
bearing_after: number;
|
||||
begin_shape_index: number;
|
||||
cost: number;
|
||||
end_shape_index: number;
|
||||
instruction: string;
|
||||
length: number;
|
||||
street_names?: string[];
|
||||
time: number;
|
||||
travel_mode: string;
|
||||
travel_type: string;
|
||||
type: number;
|
||||
verbal_multi_cue: boolean;
|
||||
verbal_post_transition_instruction: string;
|
||||
verbal_pre_transition_instruction: string;
|
||||
verbal_succinct_transition_instruction: string;
|
||||
lanes?: Lane[];
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user