chore: init

This commit is contained in:
2025-06-20 17:08:14 +02:00
commit 30fb523cf7
186 changed files with 13188 additions and 0 deletions

View 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>

View 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}

View 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>

View 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>

View File

View 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}

View 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>

View 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>

View 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}

View 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>

View 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 -->

View 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}

View 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}

View 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 });
}
})

View 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: ""
})

View 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;">&copy; 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;">&copy; 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>

View File

@@ -0,0 +1,2 @@
<h1>Error</h1>
<p>Invalid sidebar configuration.</p>

View 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>

View 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>

View 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}

View 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>

View 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();
}
}}>&lt;</Button>
<h2 class="text-lg font-bold flex gap-2 items-center">{@render children?.()}</h2>
</div>

View 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>

View 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}
/>

View 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}
/>

View 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}
/>

View 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,
};

View 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>

View File

@@ -0,0 +1,2 @@
export { default as Badge } from "./badge.svelte";
export { badgeVariants, type BadgeVariant } from "./badge.svelte";

View 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}

View 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,
};

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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,
};

View 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>

View 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}
/>

View 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>

View 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>

View 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}
/>

View 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}
/>

View 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}
/>

View 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}
/>

View 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>

View 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}
/>

View 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,
};

View 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} />

View 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>

View 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}
/>

View 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>

View 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>

View 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}
/>

View 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}
/>

View 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} />

View 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,
};

View 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} />

View 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>

View 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}
/>

View 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>

View 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>

View 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} />

View 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}
/>

View 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}
/>

View 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} />

View 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} />

View 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,
};

View File

@@ -0,0 +1,7 @@
import Root from "./input.svelte";
export {
Root,
//
Root as Input,
};

View 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}

View 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,
};

View 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>

View 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}
/>

View 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,
};

View 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>

View 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>

View 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} />

View 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>

View 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>

View File

@@ -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>

View 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>

View 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}
/>

View 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>

View File

@@ -0,0 +1,7 @@
import Root from "./separator.svelte";
export {
Root,
//
Root as Separator,
};

View 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}
/>